Português

Desbloqueie o poder das estruturas de dados imutáveis em TypeScript com tipos readonly. Aprenda a criar aplicações mais previsíveis, manuteníveis e robustas, prevenindo mutações de dados indesejadas.

Tipos Readonly do TypeScript: Dominando Estruturas de Dados Imutáveis

No cenário em constante evolução do desenvolvimento de software, a busca por um código robusto, previsível e manutenível é um esforço constante. O TypeScript, com seu sistema de tipagem forte, fornece ferramentas poderosas para alcançar esses objetivos. Entre essas ferramentas, os tipos readonly destacam-se como um mecanismo crucial para impor a imutabilidade, um pilar da programação funcional e uma chave para construir aplicações mais confiáveis.

O que é Imutabilidade e Por Que Ela é Importante?

Imutabilidade, em sua essência, significa que uma vez que um objeto é criado, seu estado não pode ser alterado. Este conceito simples tem implicações profundas na qualidade e manutenibilidade do código.

Tipos Readonly em TypeScript: Seu Arsenal de Imutabilidade

O TypeScript oferece várias maneiras de impor a imutabilidade usando a palavra-chave readonly. Vamos explorar as diferentes técnicas e como elas podem ser aplicadas na prática.

1. Propriedades Readonly em Interfaces e Tipos

A maneira mais direta de declarar uma propriedade como readonly é usar a palavra-chave readonly diretamente em uma interface ou definição de tipo.


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // Erro: Não é possível atribuir a 'id' porque é uma propriedade somente de leitura.
person.name = "Bob"; // Isto é permitido

Neste exemplo, a propriedade id é declarada como readonly. O TypeScript impedirá qualquer tentativa de modificá-la após a criação do objeto. As propriedades name e age, sem o modificador readonly, podem ser modificadas livremente.

2. O Tipo Utilitário Readonly

O TypeScript oferece um poderoso tipo utilitário chamado Readonly<T>. Este tipo genérico pega um tipo existente T e o transforma, tornando todas as suas propriedades readonly.


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // Erro: Não é possível atribuir a 'x' porque é uma propriedade somente de leitura.

O tipo Readonly<Point> cria um novo tipo onde tanto x quanto y são readonly. Esta é uma maneira conveniente de tornar rapidamente um tipo existente imutável.

3. Arrays Readonly (ReadonlyArray<T>) e readonly T[]

Arrays em JavaScript são inerentemente mutáveis. O TypeScript fornece uma maneira de criar arrays somente de leitura usando o tipo ReadonlyArray<T> ou a forma abreviada readonly T[]. Isso impede a modificação do conteúdo do array.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // Erro: A propriedade 'push' não existe no tipo 'readonly number[]'.
// numbers[0] = 10; // Erro: A assinatura de índice no tipo 'readonly number[]' permite apenas a leitura.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // Equivalente a ReadonlyArray
// moreNumbers.push(11); // Erro: A propriedade 'push' não existe no tipo 'readonly number[]'.

Tentar usar métodos que modificam o array, como push, pop, splice, ou atribuir diretamente a um índice, resultará em um erro do TypeScript.

4. const vs. readonly: Entendendo a Diferença

É importante distinguir entre const e readonly. const impede a reatribuição da variável em si, enquanto readonly impede a modificação das propriedades do objeto. Eles servem a propósitos diferentes e podem ser usados juntos para máxima imutabilidade.


const immutableNumber = 42;
// immutableNumber = 43; // Erro: Não é possível reatribuir à variável const 'immutableNumber'.

const mutableObject = { value: 10 };
mutableObject.value = 20; // Isto é permitido porque o *objeto* não é const, apenas a variável.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // Erro: Não é possível atribuir a 'value' porque é uma propriedade somente de leitura.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // Erro: Não é possível reatribuir à variável const 'constReadonlyObject'.
// constReadonlyObject.value = 60; // Erro: Não é possível atribuir a 'value' porque é uma propriedade somente de leitura.

Como demonstrado acima, const garante que a variável sempre aponte para o mesmo objeto na memória, enquanto readonly garante que o estado interno do objeto permaneça inalterado.

Exemplos Práticos: Aplicando Tipos Readonly em Cenários do Mundo Real

Vamos explorar alguns exemplos práticos de como os tipos readonly podem ser usados para aprimorar a qualidade e a manutenibilidade do código em vários cenários.

1. Gerenciando Dados de Configuração

Dados de configuração são frequentemente carregados uma vez no início da aplicação e não devem ser modificados durante a execução. Usar tipos readonly garante que esses dados permaneçam consistentes e impede modificações acidentais.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... use config.timeout e config.apiUrl com segurança, sabendo que eles não mudarão
}

fetchData("/data", config);

2. Implementando Gerenciamento de Estado do tipo Redux

Em bibliotecas de gerenciamento de estado como o Redux, a imutabilidade é um princípio fundamental. Tipos readonly podem ser usados para garantir que o estado permaneça imutável e que os redutores retornem apenas novos objetos de estado, em vez de modificar os existentes.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // Retorna um novo objeto de estado
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // Retorna um novo objeto de estado com itens atualizados
    default:
      return state;
  }
}

3. Trabalhando com Respostas de API

Ao buscar dados de uma API, muitas vezes é desejável tratar os dados da resposta como imutáveis, especialmente se você os estiver usando para renderizar componentes da UI. Tipos readonly podem ajudar a prevenir mutações acidentais dos dados da API.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // Erro: Não é possível atribuir a 'completed' porque é uma propriedade somente de leitura.
});

4. Modelando Dados Geográficos (Exemplo Internacional)

Considere a representação de coordenadas geográficas. Uma vez que uma coordenada é definida, ela idealmente deve permanecer constante. Isso garante a integridade dos dados, particularmente ao lidar com aplicações sensíveis como sistemas de mapeamento ou navegação que operam em diferentes regiões geográficas (por exemplo, coordenadas de GPS para um serviço de entrega que abrange América do Norte, Europa e Ásia).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // Imagine um cálculo complexo usando latitude e longitude
 // Retornando valor de espaço reservado para simplicidade
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Distância entre Tóquio e Nova York (espaço reservado):", distance);

// tokyoCoordinates.latitude = 36.0; // Erro: Não é possível atribuir a 'latitude' porque é uma propriedade somente de leitura.

Tipos Profundamente Readonly: Lidando com Objetos Aninhados

O tipo utilitário Readonly<T> torna apenas as propriedades diretas de um objeto readonly. Se um objeto contém objetos ou arrays aninhados, essas estruturas aninhadas permanecem mutáveis. Para alcançar uma verdadeira imutabilidade profunda, você precisa aplicar recursivamente Readonly<T> a todas as propriedades aninhadas.

Aqui está um exemplo de como criar um tipo profundamente readonly:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // Erro
// company.address.city = "New City"; // Erro
// company.employees.push("Charlie"); // Erro

Este tipo DeepReadonly<T> aplica recursivamente Readonly<T> a todas as propriedades aninhadas, garantindo que toda a estrutura do objeto seja imutável.

Considerações e Trade-offs

Embora a imutabilidade ofereça benefícios significativos, é importante estar ciente dos possíveis trade-offs.

Bibliotecas para Estruturas de Dados Imutáveis

Várias bibliotecas podem simplificar o trabalho com estruturas de dados imutáveis em TypeScript:

Melhores Práticas para Usar Tipos Readonly

Para aproveitar efetivamente os tipos readonly em seus projetos TypeScript, siga estas melhores práticas:

Conclusão: Abraçando a Imutabilidade com os Tipos Readonly do TypeScript

Os tipos readonly do TypeScript são uma ferramenta poderosa para construir aplicações mais previsíveis, manuteníveis e robustas. Ao abraçar a imutabilidade, você pode reduzir o risco de bugs, simplificar a depuração e melhorar a qualidade geral do seu código. Embora existam alguns trade-offs a serem considerados, os benefícios da imutabilidade geralmente superam os custos, especialmente em projetos complexos e de longa duração. À medida que você continua sua jornada com o TypeScript, torne os tipos readonly uma parte central do seu fluxo de trabalho de desenvolvimento para desbloquear todo o potencial da imutabilidade e construir software verdadeiramente confiável.