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