Implement `DeepReadonly<T>` so every property, at every depth, becomes `readonly`. Functions must pass through untouched — marking their properties readonly would change nothing and break the equality check.
01
Try the puzzle yourself
Par 4
Puzzle
deep-readonly.ts
type DeepReadonly<T> = ???
Stroke 1 of 4Not run yet
Replace ??? — your solution is checked against the cases below. Tab indents; press Escape then Tab to move focus out.
Checks
4
DeepReadonly<{ a: number }>—
→ { readonly a: number }
DeepReadonly<{ a: { b: string } }>—
→ { readonly a: { readonly b: string } }
DeepReadonly<string>—
→ string
DeepReadonly<{ f: () => void }>—
→ { readonly f: () => void }
How a check is judged Exact type equality, not assignability — an intersection is not the same as the flattened object.
How everyone did
Fewer than 5 people have solved this one so far. The distribution appears once there is enough of a sample to mean anything.
type DeepReadonly<T> = T extends (...args: never[]) => unknown
? T
: T extendsobject
? { readonly [K inkeyof T]: DeepReadonly<T[K]> }
: T
The common wrong answer
type DeepReadonly<T> = T extendsobject
? { readonly [K inkeyof 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.