What the heck is a homomorphic mapped kind? · andrea simone costa

I bear in mind again within the day once I stumbled upon the time period homomorphic for the primary time within the good ol’ TypeScript handbook. Actually, the handbook’s clarification was a bit fuzzy to me.
After itemizing a few instance mapped sorts:
kind Nullable<T> = null ;
kind Partial<T> = { [P in keyof T]?: T[P] };
The handbook continued by saying:
In these examples, the properties listing is
keyof T
and the ensuing kind is a few variant ofT[P]
. This can be a good template for any normal use of mapped sorts. That’s as a result of this type of transformation is homomorphic, which signifies that the mapping applies solely to properties ofT
and no others.
Instantly afterward, it claimed that even Choose<T, Okay extends keyof T> = { [P in K]: T[P]; }
is homomorphic, whereas Report
isn’t:
Readonly
,Partial
andChoose
are homomorphic whereasReport
isn’t. One clue thatReport
isn’t homomorphic is that it doesn’t take an enter kind to repeat properties from. Non-homomorphic sorts are primarily creating new properties, […].
The time period homomorphic is a little bit of a stretch from its math roots, however it’s principally saying that this type of mapped kind retains the unique kind’s construction intact. In reality, the TypeScript wiki states:
Mapped sorts declared as
{ [ K in keyof T ]: U }
the placeT
is a kind parameter are often known as homomorphic mapped sorts, which signifies that the mapped kind is a construction preserving operate ofT
.
Wanting again, after getting cozy with the kind system, the handbook’s clarification makes extra sense now. However hey, there’s presently no up-to-date and full definition. The brand new handbook doesn’t even point out the time period homomorphic, however it does seem within the supply code.
I used to be simply bored with not having the complete image, so I opened up the compiler and tried to determine as soon as and for all what the heck a homomorphic mapped kind is.
getHomomorphicTypeVariable
Link to heading
Right here’s the operate that helps us reply the query:
operate getHomomorphicTypeVariable(kind: MappedType) {
const constraintType = getConstraintTypeFromMappedType(kind);
if (constraintType.flags & TypeFlags.Index) {
const typeVariable = getActualTypeVariable((constraintType as IndexType).kind);
if (typeVariable.flags & TypeFlags.TypeParameter) {
return typeVariable as TypeParameter;
}
}
return undefined;
}
A mapped kind { [P in C]: ... }
is homomorphic if its constraint C
is only a keyof T
, the place T
should be a kind variable. That is indicated by the TypeFlags.Index
and TypeFlags.TypeParameter
flags, respectively. The place does the kind variable come from? It may very well be declared as enter or inferred utilizing the infer
key phrase. So, the examples from the previous handbook are all good, aside from Choose
, which it appears TypeScript not considers homomorphic.
So, what properties do homomorphic mapped sorts have? Oh, and what concerning the as
clause? It permits us to rename and even take away keys, theoretically altering the thing’s construction.
instantiateMappedType
Link to heading
This operate comes into play when it’s essential to instantiate a mapped kind:
operate instantiateMappedType(kind: MappedType, mapper: TypeMapper, aliasSymbol?: Image, aliasTypeArguments?: readonly Sort[]): Sort {
// For a homomorphic mapped kind { [P in keyof T]: X }, the place T is a few kind variable, the mapping
// operation relies on T as follows:
// * If T is a primitive kind no mapping is carried out and the result's merely T.
// * If T is a union kind we distribute the mapped kind over the union.
// * If T is an array we map to an array the place the factor kind has been reworked.
// * If T is a tuple we map to a tuple the place the factor sorts have been reworked.
// * In any other case we map to an object kind the place the kind of every property has been reworked.
// For instance, when T is instantiated to a union kind A | B, we produce { [P in keyof A]: X } |
// { [P in keyof B]: X }, and when when T is instantiated to a union kind A | undefined, we produce
// { [P in keyof A]: X } | undefined.
const typeVariable = getHomomorphicTypeVariable(kind);
if (typeVariable) {
const mappedTypeVariable = instantiateType(typeVariable, mapper);
if (typeVariable !== mappedTypeVariable) {
return mapTypeWithAlias(
getReducedType(mappedTypeVariable),
t => {
if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && !isErrorType(t)) {
if (!kind.declaration.nameType) {
let constraint;
if (
isArrayType(t) || t.flags & TypeFlags.Any && findResolutionCycleStartIndex(typeVariable, TypeSystemPropertyName.ImmediateBaseConstraint) < 0
&& (constraint = getConstraintOfTypeParameter(typeVariable)) && everyType(constraint, isArrayOrTupleType)
) {
return instantiateMappedArrayType(t, kind, prependTypeMapping(typeVariable, t, mapper));
}
if (isGenericTupleType(t)) {
return instantiateMappedGenericTupleType(t, kind, typeVariable, mapper);
}
if (isTupleType(t)) {
return instantiateMappedTupleType(t, kind, prependTypeMapping(typeVariable, t, mapper));
}
}
return instantiateAnonymousType(kind, prependTypeMapping(typeVariable, t, mapper));
}
return t;
},
aliasSymbol,
aliasTypeArguments,
);
}
}
// If the constraint kind of the instantiation is the wildcard kind, return the wildcard kind.
return instantiateType(getConstraintTypeFromMappedType(kind), mapper) === wildcardType ? wildcardType : instantiateAnonymousType(kind, mapper, aliasSymbol, aliasTypeArguments);
}
Right here’s the catch: homomorphic mapped sorts are dealt with in a particular manner, and you may observe this by inspecting the primary if assertion. Feedback assist us perceive a few of their particular properties:
-
if the homomorphic mapped kind is utilized to a primitive kind, the result’s the primitive kind itself
HMT<1> = 1 HMT<string> = string
-
if the homomorphic mapped kind is utilized to a union kind, the result’s the union of the mapped kind utilized to every member of the union (subsequently, TS usually calls homomorphic mapped sorts distributive)
HMT<A | B> = HTM<A> | HTM<B>
-
if the homomorphic mapped kind is utilized to an array, the end result remains to be an array the place the factor kind has been reworked by the logic of the mapped kind
kind HMT<T> = { [P in keyof T]: F<T[P]> } HMT<A[]> = F<A>[]
-
if the homomorphic mapped kind is utilized to a tuple, the end result remains to be a tuple the place the factor sorts have been reworked by the logic of the mapped kind
kind HMT<T> = { [P in keyof T]: F<T[P]> } HMT<[A, B, C]> = [F<A>, F<B>, F<C>]
Principally, an homomorphic mapped kind – with out an as
clause – iterates solely over the numeric (quantity | `${quantity}`
) keys of the array (tuple) kind, leaving the opposite keys untouched. Due to this fact the mapped kind logic is utilized solely on factor sorts.
The preservation of tuple and array sorts occurs provided that !kind.declaration.nameType
. Should you use the as
clause, then kind.declaration.nameType
incorporates no matter follows the clause, like a template literal or a conditional. It is sensible to lose tuple and array sorts if we rename or filter out some keys, as we might doubtless lose some or all of the numeric keys. With an as
clause, even a homomorphic mapped kind presently iterates by all of the keys of the array (tuple) kind, however this could change soon.
Due to this fact, utilizing the as
clause doesn’t disqualify a mapped kind from being homomorphic. It merely doesn’t protect tuple and array sorts.
resolveMappedTypeMembers and getModifiersTypeFromMappedType
Link to heading
In brief phrases, any mapped kind of the shape { [P in keyof T]: ... }
, the place T
could also be a kind variable or not, is ready to protect the modifiers of the unique kind T
, that is named the modifiers kind. As a result of all homomorphic mapped sorts respect that kind, they do protect the modifiers:
kind HMT<T> = { [P in keyof T]: F<T[P]> }
HMT<{ readonly a: A, b?: B }> = { readonly a: F<A>, b?: F<B> }
If a mapped kind has the shape { [P in C]: ... }
the place C
is a kind parameter and the costraint of C
is keyof T
, then the modifiers kind is T
. This let utility sorts like Choose
protect the modifiers of the unique kind, though they don’t seem to be homomorphic:
kind Choose<T, Okay extends keyof T> = { [P in K]: T[P]; }
Choose<{ readonly a: A, b?: B }, "a"> = { readonly a: A }
Moreover, homomorphic mapped sorts might protect the symlinks between unique and derived properties as properly. Symlinks allow image navigation within the IDE (issues like “go to definition”). Even this property isn’t unique to homomorphic mapped sorts: if modifiers might be preserved, then the opportunity of sustaining the hyperlinks can be being thought of.
The next code snippet is taken from resolveMappedTypeMembers
:
// stuff...
const shouldLinkPropDeclarations = getMappedTypeNameTypeKind(mappedType) !== MappedTypeNameTypeKind.Remapping;
const modifiersType = getModifiersTypeFromMappedType(kind); // skipping some particulars
// different stuff...
const modifiersProp = something_something(modifiersType, ...); // skipping different particulars
// far more stuff...
if (modifiersProp) {
prop.hyperlinks.syntheticOrigin = modifiersProp;
prop.declarations = shouldLinkPropDeclarations ? modifiersProp.declarations : undefined;
}
So, all the pieces revolves across the worth of shouldLinkPropDeclarations
. This flag is false
provided that we’re utilizing an as
clause for key remapping. In that case, the hyperlinks are misplaced. If an as
clause is employed only for key filtering or no as
clause is used in any respect, then the hyperlinks are preserved, offered that modifiersProp
isn’t falsy.
inferFromObjectTypes
Link to heading
Have you ever ever heard about reverse mapped sorts? If not, verify this superior speak by Mateusz Burzyński at TypeScript Congress 2023: Infer multiple things at once with reverse mapped types.
I chorus from posting your entire operate, as a result of it’s in depth. Relating to the opportunity of reversing the motion of a mapped kind, nonetheless, the essence lies within the following traces:
if (getObjectFlags(goal) & ObjectFlags.Mapped && !(goal as MappedType).declaration.nameType) {
const constraintType = getConstraintTypeFromMappedType(goal as MappedType);
if (inferToMappedType(supply, goal as MappedType, constraintType)) {
return;
}
}
As soon as once more, we’ve !(goal as MappedType).declaration.nameType
, which prevents the reversion within the case of utilizing the as
clause. Whereas being homomorphic isn’t an absolute requirement for reversion, as a result of even some non-homomorphic mapped sorts might be reverted, it does function a great indicator that TypeScript may pull off the reversion if there isn’t any as
clause.
Achtung: this is likely to be enhanced quickly, due to this PR. Filtering mapped sorts are simpler to revert than renaming mapped sorts, so the as
clause won’t an enormous live performance anymore in the event you use it only for filter out some keys.
In conclusion, homomorphic mapped sorts are people who take the shape { [K in keyof T (as ...)]: ... }
, the place T
is a kind variable, and the parentheses point out that the as
clause is non-obligatory. Homomorphic mapped sorts with out the as
clause are the cream of the crop, boasting particular properties; these with the as
clause aren’t that dangerous, however they arrive with just a few much less options. If a mapped kind isn’t homomorphic, it’d nonetheless have some properties, like preserving modifiers, having symlinks to the unique kind, and the opportunity of being reverted.
When crafting a mapped kind, goal for homomorphism.