Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptProgramação Funcional

Imutabilidade em TypeScript

Por 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 desconto

readonly — 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 property

as 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.city

Immer — 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

Referências

On this page