Apply DeepReadonly to JSON-Generated TypeScript Types

Use json to typescript with a DeepReadonly utility to recursively freeze nested objects and arrays — perfect for Redux state and config files.

TypeScript Patterns

Detailed Explanation

Shallow vs Deep Readonly

Readonly<T> only freezes the top level. Nested objects and arrays remain mutable, which is rarely what you want for state slices loaded from JSON config or API responses.

Example JSON

{
  "site": {
    "name": "DevToolbox",
    "locales": ["en", "ja"],
    "theme": { "primary": "#3b82f6", "radius": 8 }
  },
  "features": [
    { "key": "betaSearch", "enabled": true }
  ]
}

Generated TypeScript

interface Theme {
  primary: string;
  radius: number;
}

interface Site {
  name: string;
  locales: string[];
  theme: Theme;
}

interface Feature {
  key: string;
  enabled: boolean;
}

interface Config {
  site: Site;
  features: Feature[];
}

A DeepReadonly Utility

type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type FrozenConfig = DeepReadonly<Config>;

FrozenConfig rejects config.features.push(...), config.site.theme.primary = "#000", and config.site.locales[0] = "fr" — all at compile time.

When DeepReadonly Pays Off

  • Redux/Zustand state — Catches accidental in-place mutations that bypass the reducer.
  • Config files loaded once at boot — Prevents code from accidentally rewriting site settings.
  • Snapshot testing — Guarantees a snapshot value cannot be mutated between assertions.

Caveats

  • DeepReadonly is purely a compile-time view. Object.freeze is needed for runtime enforcement.
  • It does not handle Map, Set, or class instances out of the box — extend the conditional types if your config uses them.
  • Applying DeepReadonly to a discriminated union still works because the conditional type recurses through object first, then literal types pass through unchanged.

Pair this utility with the JSON-generated types and you get one immutable view of your entire config tree from a single Config interface.

Use Case

Loading a site configuration JSON at server start in Next.js and ensuring no request handler can accidentally mutate the config tree halfway through rendering.

Try It — JSON to TypeScript

Open full tool