Readonly deep: imutabilidade profunda
1. Introdução à Imutabilidade e Readonly Nativo
Imutabilidade é um dos pilares da programação funcional e uma prática cada vez mais adotada em aplicações TypeScript modernas. Quando um objeto é imutável, seu estado não pode ser alterado após a criação — qualquer "modificação" gera um novo objeto. Isso elimina efeitos colaterais, facilita o rastreamento de mudanças e torna o código mais previsível, especialmente em aplicações com estado global como Redux ou Zustand.
O TypeScript oferece o tipo utilitário Readonly<T> para marcar propriedades como somente leitura:
type User = {
name: string;
address: {
city: string;
zip: string;
};
};
const user: Readonly<User> = {
name: "Alice",
address: { city: "SP", zip: "01000" }
};
user.name = "Bob"; // Erro: não pode atribuir a 'name' porque é readonly
Porém, Readonly é superficial. Veja o problema:
user.address.city = "RJ"; // ✅ Compila! Mutação ocorre sem erros
O TypeScript não reclama porque Readonly apenas torna as propriedades do primeiro nível imutáveis. Propriedades aninhadas permanecem mutáveis. Essa limitação é a porta de entrada para bugs silenciosos.
2. O Problema da Imutabilidade Superficial
Em cenários reais, a mutação acidental em objetos aninhados é comum. Considere um estado de aplicação:
type AppState = {
user: {
profile: { name: string; email: string };
preferences: { theme: "light" | "dark" };
};
ui: { sidebarOpen: boolean };
};
function updateTheme(state: Readonly<AppState>, theme: "light" | "dark") {
// Intenção: retornar novo estado
state.user.preferences.theme = theme; // ❌ Mutação direta, sem erro de compilação
return state;
}
Aqui, Readonly falha em proteger o estado. O contrato de imutabilidade é quebrado sem que o TypeScript aponte o problema. As consequências incluem:
- Bugs intermitentes: componentes React podem não re-renderizar corretamente
- Comportamento imprevisível: funções puras tornam-se impuras
- Dificuldade de debug: a origem da mutação é difícil de rastrear
O que realmente precisamos é de um tipo ReadonlyDeep que torne todas as propriedades, em qualquer profundidade, imutáveis.
3. Implementando ReadonlyDeep com Tipos Recursivos
A solução está nos tipos condicionais recursivos do TypeScript. Vamos construir nosso próprio ReadonlyDeep:
type ReadonlyDeep<T> = T extends (infer U)[]
? ReadonlyArray<ReadonlyDeep<U>>
: T extends Function
? T
: T extends object
? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }
: T;
Vamos analisar cada parte:
- Arrays: se
Té um array (T extends (infer U)[]), aplicamosReadonlyArrayrecursivamente nos elementos - Funções: funções não devem ser transformadas — mantemos o tipo original
- Objetos: para qualquer objeto, tornamos cada propriedade
readonlye aplicamos recursão - Primitivos: strings, numbers, booleans etc. são mantidos como estão
Exemplo de uso:
type DeepUser = ReadonlyDeep<User>;
const user: DeepUser = {
name: "Alice",
address: { city: "SP", zip: "01000" }
};
user.address.city = "RJ"; // ❌ Erro: não pode atribuir a 'city' porque é readonly
Para arrays e tuplas:
type Data = { items: { id: number; value: string }[] };
type DeepData = ReadonlyDeep<Data>;
const data: DeepData = { items: [{ id: 1, value: "a" }] };
data.items[0].value = "b"; // ❌ Erro: readonly
data.items.push({ id: 2, value: "c" }); // ❌ Erro: 'push' não existe em ReadonlyArray
4. Lidando com Casos Complexos e Limitações
Union types e discriminated unions
ReadonlyDeep lida bem com union types, aplicando recursão em cada membro:
type Result = { status: "success"; data: string } | { status: "error"; message: string };
type DeepResult = ReadonlyDeep<Result>;
// Funciona corretamente para ambos os casos
Tipos opcionais e undefined
Propriedades opcionais são preservadas:
type Config = { timeout?: number; headers: Record<string, string> };
type DeepConfig = ReadonlyDeep<Config>;
// timeout continua opcional, headers é profundamente readonly
Limitações conhecidas
- Referências circulares: tipos como
type TreeNode = { value: number; children: TreeNode[] }podem causar erro de recursão infinita anyeunknown:ReadonlyDeep<any>resolve paraany, perdendo a proteçãoMap,Set,Promise: objetos nativos com métodos mutáveis não são cobertos — seria necessário tratamento específico
5. Integração com Bibliotecas e Padrões de Projeto
Usando com Immer
O Immer permite "mutar" estados imutáveis com draft. Combinado com ReadonlyDeep, temos segurança em tempo de compilação e runtime:
import { produce } from "immer";
type State = ReadonlyDeep<{ user: { name: string; score: number } }>;
const baseState: State = { user: { name: "Alice", score: 0 } };
const nextState = produce(baseState, (draft) => {
draft.user.score = 10; // ✅ Permitido pelo Immer, mas draft é ReadonlyDeep
});
Padrão Redux/Zustand
Em Redux, reducers devem ser puros. ReadonlyDeep garante que nenhum reducer muta o estado:
type AppState = ReadonlyDeep<{ todos: { id: number; text: string; done: boolean }[] }>;
function todosReducer(state: AppState, action: { type: "TOGGLE"; id: number }): AppState {
// Sem ReadonlyDeep, poderíamos fazer state.todos.find(t => t.id === action.id)!.done = true
return {
...state,
todos: state.todos.map(t => t.id === action.id ? { ...t, done: !t.done } : t)
};
}
Combinação com as const e satisfies
const config = {
api: { url: "https://api.example.com", retries: 3 },
debug: false
} as const satisfies ReadonlyDeep<typeof config>;
// config é profundamente readonly e com tipos literais
6. Performance, Compilação e Boas Práticas
Impacto na compilação
Tipos recursivos profundos podem aumentar o tempo de compilação. Para objetos com mais de 5-6 níveis de aninhamento, considere limitar a profundidade:
type ReadonlyDeepLimited<T, Depth extends number = 3> = Depth extends 0
? T
: T extends (infer U)[]
? ReadonlyArray<ReadonlyDeepLimited<U, Subtract<Depth, 1>>>
: T extends object
? { readonly [K in keyof T]: ReadonlyDeepLimited<T[K], Subtract<Depth, 1>> }
: T;
Boas práticas
- Use
Readonlypara objetos simples (1 nível) — é mais rápido e legível - Reserve
ReadonlyDeeppara: - Estados globais de aplicação
- Dados vindos de APIs externas
- Configurações que não devem ser alteradas
- Evite em:
- Tipos com referências circulares
- Objetos com mais de 10 níveis de profundidade
- Props de componentes React (a menos que necessário)
7. Ferramentas e Alternativas
Bibliotecas populares
-
type-fest: ofereceReadonlyDeepmaduro e testado
typescript import type { ReadonlyDeep } from 'type-fest'; -
ts-essentials: alternativa comDeepReadonlyObjecte suporte aMap/Set
Comparação com runtime
Object.freeze congela objetos em runtime, mas não oferece proteção em tempo de compilação:
const frozen = Object.freeze({ user: { name: "Alice" } });
frozen.user.name = "Bob"; // ✅ Compila, mas falha silenciosamente em strict mode
A combinação ideal: ReadonlyDeep para proteção em compilação + Object.freeze para proteção em runtime.
Alternativas futuras
Há discussões na comunidade TypeScript (TC39/TypeScript) sobre adicionar ReadonlyDeep como tipo nativo. Enquanto isso não acontece, as soluções manuais ou de bibliotecas são suficientes.
8. Conclusão e Próximos Passos
A imutabilidade profunda com ReadonlyDeep transforma a forma como escrevemos TypeScript. Ela elimina uma classe inteira de bugs relacionados a mutação acidental, torna o código mais declarativo e facilita a adoção de padrões como Redux e Immer.
Checklist para implementação
- [ ] Avalie se seu projeto precisa de imutabilidade profunda
- [ ] Escolha entre implementação manual ou biblioteca (
type-fest) - [ ] Defina tipos globais com
ReadonlyDeeppara estados - [ ] Combine com
Object.freezeem pontos críticos - [ ] Monitore o tempo de compilação e ajuste a profundidade máxima
Próximos passos
Explore temas vizinhos como Option types (para lidar com nulabilidade) e Phantom types (para segurança de tipos em runtime). A combinação dessas técnicas leva a um sistema de tipos que previne bugs antes mesmo de o código ser executado.
Referências
- TypeScript Handbook: Utility Types - Readonly — Documentação oficial do tipo
Readonlye suas limitações - type-fest: ReadonlyDeep — Implementação madura e testada de
ReadonlyDeepna biblioteca type-fest - Immer Documentation: Curried producers and readonly — Como usar Immer com tipos readonly para imutabilidade segura
- TypeScript Deep Dive: Recursive conditional types — Guia detalhado sobre tipos condicionais recursivos no TypeScript
- Redux Style Guide: Immutability — Boas práticas de imutabilidade no ecossistema Redux
- TypeScript 4.1: Recursive Conditional Types — Anúncio oficial do TypeScript 4.1 que tornou possível implementar
ReadonlyDeep