Repository navigation
Design Meeting Notes, 3/8/2022 #53169
Description
Activity
- addedDesign NotesNotes from our design meetingsNotes from our design meetings
on Mar 8, 2023 Reads like a good novel. It has everything:
- excitation
- fear of unknown
- turn of events
- suspense
Reacted by Toni Villena and ExE BossReacted by Bruce Pascoe, Joost Koehoorn, Toni Villena, Joshua Chen, Gonçalo Fernandes, Ryan Cavanaugh, Jon Bullock, ritschwumm, rubiesonthesky, maksym and 12 moreReacted by Kostiantyn KoThe problem with the "homomorphic" version of
Omitis that we lose the docblock and the goto feature:type Omit2<T, DroppedKeys extends PropertyKey> = { [K in keyof T as Exclude<K, DroppedKeys>]: T[K]; }; function omit2<O, Mask extends { [K in keyof O]?: true }>( _object: O, _mask: Mask ): Omit2<O, keyof Mask> { throw new Error("Not implemented"); } const object2 = omit2(object, { name: true, } ); object2.id // ❌ goto/docblock
Reacted by ExE BossAndarist commented
on Mar 10, 2023 ContributorMore actionsHm, I expected that to work in 5.0 since #51650 . It doesn't though... I'll investigate what's happening there
Andarist commented
on Mar 11, 2023 ContributorMore actionsIt seems that my fix (#51650) had a bug. Accidentally, my other PR already fixes this but since its fate is still not determined, I will prepare a separate PR fixing this tomorrow.
I was often confused about this in the past too. In reality, mapped types are not split into two groups though (homomorphic and non-homomorphic). They are split into 3: homomorphic, non-homomorphic with non-unknown modifiers type and non-homomorphic with an unknown moifiers type.
Homomorphic mapped types preserve potential arrayness of the input, they distribute over unions and they preserve original modifiers. So in a sense, homomorphic mapped types are a subset of the mapped typed with non-unknown modifiers type.
I might be using this lingo slightly wrong (so don't quote me on that) but that's roughly how things are called under the hood (there is no specific name for a mapped type with a (non-)unknown modifiers type though, it's just something that is checked when "transferring" the modifiers on the output type etc).
Reacted by Joe CalzarettaOkay, so "non-homomorphic with known modifiers" is specifically what you get when you write
{[P in K]: ⋯}whereK extends keyof Tfor some genericT?Yes, that's one way to end up with this "type".
getModifiersTypeFromMappedTypein the source code is defined as follows:function getModifiersTypeFromMappedType(type: MappedType) { if (!type.modifiersType) { if (isMappedTypeWithKeyofConstraintDeclaration(type)) { // If the constraint declaration is a 'keyof T' node, the modifiers type is T. We check // AST nodes here because, when T is a non-generic type, the logic below eagerly resolves // 'keyof T' to a literal union type and we can't recover T from that type. type.modifiersType = instantiateType(getTypeFromTypeNode((getConstraintDeclarationForMappedType(type) as TypeOperatorNode).type), type.mapper); } else { // Otherwise, get the declared constraint type, and if the constraint type is a type parameter, // get the constraint of that type parameter. If the resulting type is an indexed type 'keyof T', // the modifiers type is T. Otherwise, the modifiers type is unknown. const declaredType = getTypeFromMappedTypeNode(type.declaration) as MappedType; const constraint = getConstraintTypeFromMappedType(declaredType); const extendedConstraint = constraint && constraint.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(constraint as TypeParameter) : constraint; type.modifiersType = extendedConstraint && extendedConstraint.flags & TypeFlags.Index ? instantiateType((extendedConstraint as IndexType).type, type.mapper) : unknownType; } } return type.modifiersType; }
whereas
isMappedTypeHomomorphicis just this:function isMappedTypeHomomorphic(type: MappedType) { return !!getHomomorphicTypeVariable(type); }
and
getHomomorphicTypeVariableis this:function getHomomorphicTypeVariable(type: MappedType) { const constraintType = getConstraintTypeFromMappedType(type); if (constraintType.flags & TypeFlags.Index) { const typeVariable = getActualTypeVariable((constraintType as IndexType).type); if (typeVariable.flags & TypeFlags.TypeParameter) { return typeVariable as TypeParameter; } } return undefined; }
Sorry for quoting a bunch of source code but I feel like it's just the only way to answer this precisely ;p Note that an
IndexTypemeanskeyof Something. It's also useful to inspect mapped type declaration in the https://ts-ast-viewer.com/# to learn more about how specific parts of it might be called (what is its constraint, type parameter, etc)
Using a Homomorphic Mapped Type For Omit
#53134
Today,
Omitis non-homomorphic. Will not preserve shape.keyofon its own does not retain its source type.Today, we can write
Omitwith anasclause withExclude- still considered "homomorphic" (even though at this point "homomorphic" is a misnomer).Switching
Omitat this point would likely be breaky.interfaces extends fromOmitUsing a "homomorphic"
Omitmakes code flow work better because it can produce union types.Could choose to have
Omit(original) andMappedOmit- orOmit(improved) andLegacyOmit.Omitto the "better" version is breaky - at least one package breaks.We really don't want to have 2
Omits, and want to push on an improvedOmit.Pick<T, Omit<keyof T, DroppedKeys>>Wait, should
Pickbe fixed to use a homomorphic mapped type too?Omit.If you fix
Pick, thenOmitdefined asPick<T, Exclude<keyof T, DroppedKeys>>is also homomorphic, right?Omitis not going to distribute, when we writeExclude<keyof T, DroppedKeys>So each of them need to be written as their own mapped types written in terms of
keyof T.Note: the
asclause forPickshould use an intersection, notExtractnever.