Skip to content
webtype.orgwebtype.org#215 deep-readonly · par 4

    ↑↓ move · ⏎ open · esc close

    No. 215 · July 23, 2026 · Hard

    Deep Readonly

    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
    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.

    Short game

    Fewest characters

    No public scores yet.

    Archive
    02

    Annotated solution

    Published July 24, 2026

    The 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

    1. 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`.

    2. { 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.

    Uses