Arquitetura softwareFrontend webTypeScriptProgramação Funcional
Imutabilidade em TypeScript
Baixar PDFPor que nunca modificar dados — readonly, as const, spread operator, Object.freeze e Immer.
Imutabilidade em TypeScript
Imutabilidade significa: nunca modificar um dado existente — sempre criar uma nova cópia com as mudanças. Isso elimina uma classe inteira de bugs causados por mutação inesperada.
Por que imutabilidade importa?
// ❌ Mutação — bug sutil
function applyDiscount(cart: Cart, discount: number) {
for (const item of cart.items) {
item.price = item.price * (1 - discount); // ❌ MUTA o objeto original
}
return cart;
}
const originalCart = getCart();
const discountedCart = applyDiscount(originalCart, 10);
console.log(originalCart.items[0].price); // ❌ 90 — O CARRINHO ORIGINAL FOI MODIFICADO
console.log(discountedCart.items[0].price); // 90
// Se o usuário "desfizer" o desconto... não tem como.
// O originalCart já foi alterado.// ✅ Imutabilidade — novo objeto, original intacto
function applyDiscount(cart: Cart, discount: number): Cart {
return {
...cart,
items: cart.items.map(item => ({
...item,
price: item.price * (1 - discount),
})),
};
}
const originalCart = getCart();
const discountedCart = applyDiscount(originalCart, 10);
console.log(originalCart.items[0].price); // ✅ 100 — original intacto
console.log(discountedCart.items[0].price); // 90 — cópia com descontoreadonly — Imutabilidade no tipo
Readonly em propriedades
// ❌ Sem readonly — qualquer lugar pode alterar
interface User {
id: string;
name: string;
email: string;
}
function processUser(user: User) {
user.name = 'HACKED'; // ❌ compila sem erro — mutação permitida
}
// ✅ Readonly — TS impede mutação
interface User {
readonly id: string;
readonly name: string;
readonly email: string;
}
function processUser(user: User) {
user.name = 'HACKED'; // ❌ TS erro — Cannot assign to 'name' because it is a read-only property
}Readonly<T> utility type
function processUser(user: Readonly<User>) {
user.name = 'HACKED'; // ❌ TS erro
}readonly em arrays
// ❌ Array mutável
const items: string[] = ['a', 'b', 'c'];
items.push('d'); // ✅ compila
items[0] = 'z'; // ✅ compila
// ✅ Array readonly
const items: readonly string[] = ['a', 'b', 'c'];
items.push('d'); // ❌ TS erro
items[0] = 'z'; // ❌ TS erro
// Para "adicionar", cria novo array
const newItems = [...items, 'd']; // ✅ ['a', 'b', 'c', 'd']as const — Imutabilidade profunda no valor
// ❌ Sem as const — tipos são genéricos
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
};
// config.apiUrl é string (qualquer string)
// config.timeout é number (qualquer number)
// ✅ as const — tipos são literais e propriedades são readonly
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} as const;
// config.apiUrl é 'https://api.example.com' (literal)
// config.timeout é 5000 (literal)
// config.retries é readonly
// ❌ TS erro
config.apiUrl = 'outra-url'; // Cannot assign to 'apiUrl' because it is a read-only propertyas const para enums (substitui enum)
// ❌ Enum — gera código JS, deprecated no TS 6.0+
enum OrderStatus { Pending, Processing, Shipped, Delivered }
// ✅ as const — zero runtime, type-safe
const OrderStatus = {
Pending: 'pending',
Processing: 'processing',
Shipped: 'shipped',
Delivered: 'delivered',
} as const;
type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
// 'pending' | 'processing' | 'shipped' | 'delivered'Spread operator — Criar cópias
// Objeto — shallow copy
const updated = { ...user, name: 'Novo Nome' };
// { ...user original, name sobrescrito }
// Array — shallow copy
const withNewItem = [...items, newItem];
const withoutFirst = items.slice(1);
const replaced = items.map((item, i) => i === 2 ? newItem : item);
// Array — remover item
const filtered = items.filter(item => item.id !== idToRemove);Limitação: spread é shallow (superficial)
const original = {
user: { name: 'Kaique', address: { city: 'SP' } },
};
const copy = { ...original, user: { ...original.user } };
copy.user.name = 'Outro'; // ✅ não afeta original
// ❌ MAS address ainda é compartilhado
copy.user.address.city = 'RJ'; // ❌ TAMBÉM altera original.user.address.cityImmer — Imutabilidade sem dor
Immer permite escrever código mutável que produz resultado imutável. Ele usa proxies para detectar mutações e criar cópias.
import { produce } from 'immer';
// ❌ Sem Immer — spread profundo é verboso
function addItem(state: CartState, item: CartItem): CartState {
return {
...state,
cart: {
...state.cart,
items: [...state.cart.items, item],
total: state.cart.total + item.price * item.quantity,
},
};
}
// ✅ Com Immer — parece mutação, mas é imutável
function addItem(state: CartState, item: CartItem): CartState {
return produce(state, draft => {
draft.cart.items.push(item); // "mutação" — mas Immer cria cópia
draft.cart.total += item.price * item.quantity;
});
}
// No React
const [state, dispatch] = useReducer(
produce((draft: CartState, action: CartAction) => {
switch (action.type) {
case 'ADD_ITEM':
draft.cart.items.push(action.payload);
break;
case 'REMOVE_ITEM':
draft.cart.items = draft.cart.items.filter(i => i.id !== action.payload);
break;
}
}),
initialState,
);Object.freeze — Imutabilidade em runtime
// Shallow freeze — só o primeiro nível
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
});
config.apiUrl = 'hack'; // ❌ Silenciosamente falha (ou lança erro em strict mode)
// Deep freeze manual
function deepFreeze<T>(obj: T): Readonly<T> {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null && typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}Quando imutabilidade NÃO é necessária
- Loops de performance extrema — criar cópias tem custo de memória
- Objetos que nunca escapam do escopo — se ninguém mais vê o objeto, mutação é segura
- Frameworks que gerenciam imutabilidade internamente — o React já cuida disso com state