Patrones
¿Cómo evito mutaciones bajo el primer nivel en vez de congelar solo el tipo del objeto exterior?
Conserva funciones, recorre arrays y mapea cada propiedad del objeto con el modificador readonly.
La receta
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>
La compilación compila esto y comprueba cada resultado de abajo.
Cómo funciona
- 01
T extends (...args: any[]) => unknown
Las funciones ya son valores invocables; mapear sus miembros destruiría la firma de llamada.
- 02
{ readonly [K in keyof T]: DeepReadonly<T[K]> }Cada propiedad se congela y su valor vuelve a pasar por la misma decisión.
Lo que obtienes
FrozenConfig['api']→{ readonly url: string; readonly retries: number; }FrozenConfig['tags']→readonly string[]FrozenConfig['format']→(value: number) => string
Dónde falla
Es inmutabilidad de compilación, no `Object.freeze`. También trata instancias de clase como objetos estructurales, lo que puede exponer miembros de implementación; añade casos explícitos para integrados y clases en código de dominio.
Conclusión
Los modificadores recursivos necesitan reglas de parada. Trata funciones y objetos especiales antes de la rama general.
