As Linda explains, the reason why there's no infinite recursion is that when a homomorphic mapped type is applied to a primitive type it evaluates to that primitive type. Still, it's interesting to see what happens with a non-homomorphic mapped type. To define a non-homomorphic type, you can declare 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 taking the keys from I<keyof T> instead of keyof T, you can define a simple non-recursive and non-homomorphic type:
type NonHomomorphicMap<T> = {[K in I<keyof T>]: 42}
and apply it to a bunch of other types:
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> // {}
For string and 12 the type now evaluates to an object type with keys for all the methods on string and number, and an [x: number] index signature for string.
Interestingly, functions are not treated as primitive types, but as objects without keys, so NonHomomorphicMap<() => any> equals {}. This also happens with homomorphic mapped types (i.e. BlackMagic<() => any> equals {}), so BlackMagic is not identity.
It's also possible to make a recursive non-homomorphic type similar to BlackMagic, but to see the full type on hover you need a Normalize type that fully evaluates all recursive occurrences:
type Normalize<T> =
T extends Function
? T
: T extends infer S ? {[K in keyof S]: S[K]} : never
Now a non-homomorphic BlackMagic counterpart can be defined as:
type BadMagic<T> = Normalize<{
[K in I<keyof T>]: K extends keyof T ? BadMagic<T[K]> : never
}>
Besides using Normalize and I, there's also an extra conditional K extends keyof T as TypeScript can't infer that the result of applying I can still index T. This does not affect the behavior though.
Applying 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> // {}
Most of the recursion ends at methods that turn into {}, but if you look at BadMagic<string> you can see there is in fact some "infinite" recursion, as the [x: number] property on strings is itself a string. Basically, you have
BadMagic<string> = {
[x: number]: {
[x: number]: {
[x: number]: BadMagic<string>, // "infinite"
...
},
...
},
toString: {},
charAt: {},
...
}
TypeScript playground