The type
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Function ? T[K] : DeepReadonly<T[K]>
}
is considered a homomorphic or structure-preserving mapped type, where you are explicitly mapping over the keys of another type (e.g., in keyof
). See
What does "homomorphic mapped type" mean? for more details.
According to microsoft/TypeScript#12447 (where these are called "isomorphic" instead of "homomorphic"), emphasis mine:
A mapped type of the form { [P in keyof T]: X }
, where T
is some type parameter, is said to be an isomorphic mapped type because it produces a type with the same shape as T
. ... [W]hen a primitive type is substituted for T
in an isomorphic mapped type, we simply produce that primitive type. For example, when { [P in keyof T]: X }
is instantiated with A | undefined
for T
, we produce { [P in keyof A]: X } | undefined
.
So, because string
is a primitive type, DeepReadonly<string>
evaluates immediately to string
without even consulting the body, and therefore without evaluating string[K] extends Function ? string[K] : DeepReadonly<string[K]>
.
And thus your type A
only recurses down one level before terminating:
type X1 = { a: string };
type A = DeepReadonly<X1>;
/* type A = {
readonly a: string;
} */
That answers the question as asked. Still, it's worth noting that TypeScript can represent recursive data structures without hitting a "loop" in the type instantiation:
interface Tree {
value: string;
children: Tree[]
}
type B = DeepReadonly<Tree>
/* type B = {
readonly value: string;
readonly children: readonly DeepReadonly<Tree>[];
} */
The Tree
type and therefore the B
type are defined recursively, but it doesn't break anything. There is a loop conceptually but the compiler doesn't hit a loop.
So even if DeepReadonly<string>
were not a homomorphic mapped type, nothing catastrophic would have happened; instead you'd get a fairly ugly recursive type where all of string
's apparent members would be enumerated and modified:
type NonHomomorphicDeepReadonly<T> = keyof T extends infer KK extends keyof T ? {
readonly [K in KK]: NonHomomorphicDeepReadonly<T[K]>
} : never;
type C = NonHomomorphicDeepReadonly<string>;
/* type C = {
readonly [x: number]: ...;
readonly [Symbol.iterator]: {};
readonly toString: {};
readonly charAt: {};
readonly charCodeAt: {};
readonly concat: {};
readonly indexOf: {};
readonly lastIndexOf: {};
readonly localeCompare: {};
... 34 more ...;
readonly padEnd: {};
} */
type D = C[0][0][0][0];
/* type D = {
readonly [x: number]: ...;
readonly [Symbol.iterator]: {};
readonly toString: {};
readonly charAt: {};
readonly charCodeAt: {};
readonly concat: {};
readonly indexOf: {};
readonly lastIndexOf: {};
readonly localeCompare: {};
... 34 more ...;
readonly padEnd: {};
} */
which probably isn't what anyone would want (which is why homomorphic mapped types on primitives behave the way they do), but it's a perfectly acceptable type.
Playground link to code