According to Linda's explanation, the absence of infinite recursion occurs because a homomorphic mapped type, when applied to a primitive type, results in that same primitive type. However, it is worth exploring the outcome with a non-homomorphic mapped type. To create a non-homomorphic type, you can define an identity type I
such that
I<'key1' | 'key2'> = 'key1' | 'key2'
, using a conditional type:
type I<T> = T extends infer S ? S : T
By extracting keys from I<keyof T>
rather than keyof T
, you can establish a straightforward non-recursive and non-homomorphic type:
type NonHomomorphicMap<T> = {[K in I<keyof T>]: 42}
This type can then be applied to various other types as shown below:
type TestObject = NonHomomorphicMap<{q: string}> // {q: 42}
type TestString = NonHomomorphicMap<string> // {[x: number]: 42, toString: 42, charAt: 42, ...}
type TestNum = NonHomomorphicMap<12> // {toString: 42, toFixed: 42, toExponential: 42, toPrecision: 42, valueOf: 42, toLocaleString: 42}
type TestFun = NonHomomorphicMap<() => number> // {}
In cases where the input is a string
or 12
, the resulting type transforms into an object type containing keys for all methods associated with the respective types along with an [x: number]
index signature for strings.
It's interesting to note that functions are not classified as primitive types but rather operate as keyless objects, hence
NonHomomorphicMap<() => any>
evaluates to
{}
. This behavior also applies to homomorphic mapped types such as
BlackMagic<() => any>
, illustrating that
BlackMagic
does not equate to identity.
You can also construct a recursive non-homomorphic type similar to BlackMagic
, but a Normalize
type is necessary to fully assess the complete type on hover:
type Normalize<T> =
T extends Function
? T
: T extends infer S ? {[K in keyof S]: S[K]} : never
Subsequently, a non-homomorphic equivalent to BlackMagic
can be defined as:
type BadMagic<T> = Normalize<{
[K in I<keyof T>]: K extends keyof T ? BadMagic<T[K]> : never
}>
In addition to employing Normalize
and I
, an extra conditional K extends keyof T
is included to ensure TypeScript recognizes that the output from applying I
still indexes
T</code, although this has no impact on functionality.</p>
<p>Applying <code>BadMagic
to the sample types yields:
type TestObject = BadMagic<{q: string}> // {q: {[x: number]: {[x: number]: BadMagic<string>,...}, toString: {}, charAt: {}, ...}}
type TestString = BadMagic<string> // {[x: number]: {[x: number]: {[x: number]: BadMagic<string>, ...}, toString: {}, charAt: {}, ...}, toString: {}, charAt: {}, ...}
type TestNum = BadMagic<12> // {toString: {}, toFixed: {}, toExponential: {}, toPrecision: {}, valueOf: {}, toLocaleString: {}}
type TestFun = BadMagic<() => any> // {}
The majority of recursion terminates at method properties that resolve into {}
. However, when inspecting BadMagic<string>
, some level of "infinite" recursion becomes apparent as the [x: number]
property on strings recursively refers back to itself. Essentially:
BadMagic<string> = {
[x: number]: {
[x: number]: {
[x: number]: BadMagic<string>, // "infinite"
...
},
...
},
toString: {},
charAt: {},
...
}
Explore the TypeScript playground.