← All tasksTypeScript · Senior

DeepReadonly<T>

The built-in Readonly<T> protects only the first level: config.server.port = 1 still compiles. Write DeepReadonly<T> that makes every field readonly at any depth, including arrays and their elements.

Functions stay as they are, and primitives (string, number...) come back unchanged.

Tests (5)

Type tests are checked by the TypeScript compiler in strict mode. Expect<Equal<A, B>> passes when the types match exactly, and // @ts-expect-error passes when the next line does not compile. The first run downloads the compiler, so it takes a few seconds.

  1. types
    type Config = { server: { port: number; tls: { cert: string } } };
    type _ = Expect<Equal<DeepReadonly<Config>, { readonly server: { readonly port: number; readonly tls: { readonly cert: string } } }>>;
  2. types
    type State = { items: { id: number }[] };
    type _ = Expect<Equal<DeepReadonly<State>, { readonly items: readonly { readonly id: number }[] }>>;
  3. types
    type Api = { load: (id: number) => string };
    type _ = Expect<Equal<DeepReadonly<Api>, { readonly load: (id: number) => string }>>;
  4. types
    type _ = Expect<Equal<DeepReadonly<string>, string>>;
  5. types
    const c = {} as DeepReadonly<{ a: { b: number } }>;
    // @ts-expect-error - nested fields are readonly too
    c.a.b = 2;