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
- 01
T extends (...args: any[]) => unknown
Functions are already callable values; mapping their object members would destroy the call signature.
- 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
FrozenConfig['api']→{ readonly url: string; readonly retries: number; }FrozenConfig['tags']→readonly string[]FrozenConfig['format']→(value: number) => string
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.
