Annotated solution
Published July 24, 2026The solution
type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T
The common wrong answer
type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T
Without the function guard, `() => void` matches `extends object` and gets mapped over. A function type has no own enumerable properties, so the result is `{}` — the callable signature is lost.
Line by line
T extends (...args: never[]) => unknown`never[]` in parameter position accepts any signature because parameters are checked contravariantly. This is the safe way to say "any function" without `any`.
{ readonly [K in keyof T]: DeepReadonly<T[K]> }The `readonly` modifier is added while mapping, and the value type recurses. One mapped type handles both jobs.
Takeaway
Conditional branches are tested in order, so narrower cases go first. `extends object` is broader than most people expect — arrays and functions both satisfy it.