Skip to content
Make an object deeply readonly

    ↑↓ move · ⏎ open · esc close

    Patterns

    How do I prevent mutation below the first property level instead of only freezing the outer object type?

    Preserve functions, recurse through arrays, and map every object property with a readonly modifier.

    The recipe

    type DeepReadonly<T> =
      T extends (...args: any[]) => unknown
        ? T
        : T extends readonly unknown[]
          ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
          : T extends object
            ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
            : T
    
    type Config = {
      api: { url: string; retries: number }
      tags: string[]
      format: (value: number) => string
    }
    
    type FrozenConfig = DeepReadonly<Config>

    The build compiles this and checks each result below.

    How it works

    1. 01
      T extends (...args: any[]) => unknown

      Functions are already callable values; mapping their object members would destroy the call signature.

    2. 02
      { readonly [K in keyof T]: DeepReadonly<T[K]> }

      Every property is frozen and its value is sent through the same decision again.

    What you get

    Where it goes wrong

    This is compile-time immutability, not `Object.freeze`. It also treats class instances as structural objects, which can expose implementation members in surprising ways; add explicit built-in and class cases for domain code.

    Takeaway

    Recursive modifiers need stopping rules. Handle functions and special objects before the general object branch.

    See also

    Patterns