Readonly e tipos imutáveis
1. Introdução à Imutabilidade em TypeScript
Imutabilidade é o princípio de que um objeto, uma vez criado, não pode ser modificado. Em vez de alterar um objeto existente, cria-se uma nova cópia com as mudanças desejadas. Esse conceito é fundamental para previsibilidade de código, debug simplificado e programas mais seguros em ambientes concorrentes.
No TypeScript, a imutabilidade opera em dois níveis distintos:
- Tempo de compilação: O TypeScript impede que você escreva código que tente modificar propriedades marcadas como readonly. Isso gera erros durante a compilação.
- Tempo de execução: O JavaScript não possui mecanismos nativos de imutabilidade para objetos comuns. Um
readonlydo TypeScript não impede que, em runtime, alguém modifique a propriedade usando JavaScript puro.
TypeScript oferece diversas ferramentas para trabalhar com imutabilidade: o modificador readonly, o tipo utilitário Readonly<T>, as const e a possibilidade de criar tipos recursivos para imutabilidade profunda.
2. O Modificador readonly em Propriedades
O modificador readonly pode ser aplicado diretamente em propriedades de interfaces, tipos de objeto e classes.
interface Usuario {
readonly id: number;
nome: string;
email: string;
}
const usuario: Usuario = { id: 1, nome: "Ana", email: "ana@email.com" };
usuario.nome = "Maria"; // ✅ Permitido
usuario.id = 2; // ❌ Erro: Cannot assign to 'id' because it is a read-only property
Diferença entre readonly e const: const impede a reatribuição da variável, mas não torna o objeto imutável. readonly impede a modificação da propriedade específica.
const config = { url: "https://api.com" };
config.url = "https://novo.com"; // ✅ Permitido (objeto mutável)
interface Config {
readonly url: string;
}
const config2: Config = { url: "https://api.com" };
config2.url = "https://novo.com"; // ❌ Erro
Em classes, propriedades readonly devem ser inicializadas na declaração ou no construtor:
class Circulo {
readonly pi: number = 3.14159;
readonly raio: number;
constructor(raio: number) {
this.raio = raio; // Inicialização permitida no construtor
}
}
3. O Tipo Utilitário Readonly<T>
O tipo Readonly<T> é um tipo utilitário que transforma todas as propriedades de T em readonly.
type Configuracao = {
host: string;
porta: number;
ssl: boolean;
};
type ConfiguracaoImutavel = Readonly<Configuracao>;
const config: ConfiguracaoImutavel = {
host: "localhost",
porta: 3000,
ssl: true
};
config.porta = 4000; // ❌ Erro: Cannot assign to 'porta' because it is a read-only property
Exemplo prático: Proteger estado de aplicação contra modificações acidentais.
interface EstadoApp {
usuarioLogado: boolean;
dados: Record<string, unknown>;
versao: number;
}
function congelarEstado(estado: EstadoApp): Readonly<EstadoApp> {
return Object.freeze({ ...estado });
}
const estadoAtual = congelarEstado({
usuarioLogado: true,
dados: { nome: "João" },
versao: 1
});
estadoAtual.versao = 2; // ❌ Erro em compilação
Limitação importante: Readonly<T> é superficial (shallow). Objetos aninhados ainda podem ser modificados.
interface Arvore {
valor: number;
filhos: Arvore[];
}
const arvore: Readonly<Arvore> = { valor: 1, filhos: [{ valor: 2, filhos: [] }] };
arvore.filhos.push({ valor: 3, filhos: [] }); // ✅ Permitido (array mutável)
arvore.filhos[0].valor = 10; // ✅ Permitido (objeto interno mutável)
4. Imutabilidade Profunda com Tipos Recursivos
Para imutabilidade profunda, podemos criar um tipo DeepReadonly<T> personalizado:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends Record<string, unknown> | Array<unknown>
? DeepReadonly<T[P]>
: T[P];
};
// Tratamento específico para arrays e tuplas
type DeepReadonlyArray<T extends readonly unknown[]> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
// Versão completa
type DeepReadonly<T> = T extends (...args: infer A) => infer R
? (...args: DeepReadonlyArray<A>) => DeepReadonly<R>
: T extends object
? T extends readonly unknown[]
? DeepReadonlyArray<T>
: { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
Exemplo de uso:
interface ConfigProfunda {
servidor: {
host: string;
porta: number;
};
credenciais: {
usuario: string;
senha: string;
};
plugins: string[];
}
const config: DeepReadonly<ConfigProfunda> = {
servidor: { host: "localhost", porta: 3000 },
credenciais: { usuario: "admin", senha: "123" },
plugins: ["logger", "auth"]
};
config.servidor.porta = 4000; // ❌ Erro
config.plugins.push("cache"); // ❌ Erro (array readonly profundo)
Desafios com tipos complexos: Union types e intersection types podem exigir tratamento especial, e tipos recursivos podem impactar a performance do compilador em estruturas muito profundas.
5. readonly em Arrays e Tuplas
TypeScript oferece três formas de declarar arrays readonly:
// Forma 1: readonly T[]
const arr1: readonly number[] = [1, 2, 3];
// Forma 2: ReadonlyArray<T>
const arr2: ReadonlyArray<number> = [1, 2, 3];
// Forma 3: Readonly aplicado a Array
const arr3: Readonly<number[]> = [1, 2, 3];
Métodos disponíveis vs. indisponíveis:
const numeros: readonly number[] = [1, 2, 3];
numeros[0]; // ✅ Acesso permitido
numeros.length; // ✅ Propriedade length
numeros.map(n => n * 2); // ✅ Métodos que retornam novo array
numeros.filter(n => n > 1); // ✅
numeros.concat([4]); // ✅
numeros.push(4); // ❌ Erro: push não existe em readonly array
numeros.pop(); // ❌ Erro
numeros[0] = 10; // ❌ Erro: index assignment não permitido
Tuplas readonly:
type Par = readonly [string, number];
const par: Par = ["idade", 30];
par[0] = "altura"; // ❌ Erro
par.push(40); // ❌ Erro
// Tuplas com spread
type Lista = readonly [number, ...string[]];
const lista: Lista = [1, "a", "b", "c"];
6. Imutabilidade com as const
O operador as const (const assertions) cria tipos literais e torna todas as propriedades profundamente readonly:
const config = {
url: "https://api.com",
timeout: 5000,
headers: {
"Content-Type": "application/json"
}
} as const;
// Tipo inferido:
// {
// readonly url: "https://api.com";
// readonly timeout: 5000;
// readonly headers: {
// readonly "Content-Type": "application/json";
// };
// }
config.url = "https://novo.com"; // ❌ Erro
config.headers["Content-Type"] = "text/html"; // ❌ Erro (profundamente readonly)
Comparação: as const vs. Readonly<T>:
// as const: tipos literais e imutabilidade profunda
const obj1 = { nome: "Ana", idade: 30 } as const;
// Tipo: { readonly nome: "Ana"; readonly idade: 30 }
// Readonly<T>: mantém tipos amplos, imutabilidade superficial
const obj2: Readonly<{ nome: string; idade: number }> = { nome: "Ana", idade: 30 };
// Tipo: { readonly nome: string; readonly idade: number }
as const é ideal para constantes e literais, enquanto Readonly<T> é mais flexível para tipos genéricos.
7. Boas Práticas e Padrões de Uso
Usar readonly em parâmetros de funções para garantir que a função não modifique os dados recebidos:
function processarDados(dados: readonly number[]): number {
// dados.push(10); // ❌ Erro em compilação
return dados.reduce((acc, val) => acc + val, 0);
}
Imutabilidade em APIs e contratos de interface:
interface Repositorio<T> {
readonly itens: readonly T[];
obterPorId(id: string): T | undefined;
adicionar(item: T): Repositorio<T>; // Retorna nova instância
}
class RepositorioMemo<T> implements Repositorio<T> {
readonly itens: readonly T[];
constructor(itens: T[]) {
this.itens = Object.freeze([...itens]);
}
obterPorId(id: string): T | undefined {
return this.itens.find(item => (item as any).id === id);
}
adicionar(item: T): Repositorio<T> {
return new RepositorioMemo([...this.itens, item]);
}
}
Combinando readonly com generics e constraints:
function clonar<T extends Record<string, unknown>>(obj: T): Readonly<T> {
return Object.freeze({ ...obj });
}
function mesclar<T extends readonly unknown[], U extends readonly unknown[]>(
arr1: T,
arr2: U
): readonly [...T, ...U] {
return [...arr1, ...arr2];
}
8. Limitações e Considerações Finais
Imutabilidade em tempo de compilação não é garantia em runtime. O TypeScript não gera código adicional para proteger objetos:
interface Usuario {
readonly id: number;
}
const usuario: Usuario = { id: 1 };
// Em runtime, isso funciona (embora TypeScript acuse erro)
(usuario as any).id = 2;
Para proteção em runtime, combine readonly com Object.freeze():
function criarImutavel<T extends object>(obj: T): Readonly<T> {
return Object.freeze({ ...obj });
}
Performance: Tipos recursivos como DeepReadonly<T> podem aumentar o tempo de compilação em estruturas muito complexas. Use com moderação e considere o trade-off entre segurança de tipos e performance.
Alternativas: Para imutabilidade em runtime com melhor suporte, bibliotecas como Immer permitem trabalhar com estado imutável de forma produtiva, usando um "draft" mutável que é convertido em estado imutável no final.
A imutabilidade em TypeScript é uma ferramenta poderosa para escrever código mais seguro e previsível. Dominar readonly, Readonly<T>, as const e tipos recursivos permite criar contratos claros e evitar bugs relacionados a mutações indesejadas, especialmente em aplicações complexas com estado compartilhado.
Referências
- TypeScript Handbook: Readonly Properties — Documentação oficial sobre o modificador
readonlyem propriedades de objetos. - TypeScript Utility Types: Readonly
— Documentação oficial do tipo utilitário Readonly<T>. - TypeScript Handbook: Const Assertions (as const) — Notas de lançamento do TypeScript 3.4 explicando
as const. - TypeScript Deep Readonly Type — Artigo técnico sobre implementação de tipos
DeepReadonlyrecursivos. - Immer: The Immer Documentation — Documentação da biblioteca Immer para imutabilidade prática em runtime.
- TypeScript: ReadonlyArray and Readonly Tuples — Documentação oficial sobre arrays e tuplas readonly.
- Understanding TypeScript's Readonly Modifier — Artigo do LogRocket explicando padrões de uso do
readonlyem TypeScript.