TypeScript: Imutabilidade em tempo de compilação em TypeScript
Tags: ptbr, typescript, javascript, webdev
Published: 2019-12-11 05:17:56

O [TypeScript](https://www.sitepoint.com/introduction-to-typescript/) nos permite decorar o [ECMAScript compatível com as especificações](https://www.ecma-international.org/ecma-262/) com informações de tipo onde podemos analisar e gerar JavaScript usando um compilador dedicado. Em projetos de larga escala, esse tipo de análise estática pode detectar possíveis erros antes de recorrer a longas sessões de depuração, e muito menos em produção. No entanto, os tipos de referência no TypeScript ainda são mutáveis, o que pode levar a efeitos colaterais indesejados em nosso software.

Neste artigo, veremos possíveis maneiras de proibir referências de serem mutadas, o que pode ser benéfico.

>  Precisa de uma atualização sobre imutabilidade em JavaScript? Leia o nosso guia, [Imutabilidade em JavaScript](https://www.sitepoint.com/immutability-javascript/).

# Tipos Primitivos vs Tipos de Referência

JavaScript define dois grupos abrangentes de tipos de dados:

- Primitivos: valores de baixo nível que são imutáveis (por exemplo, strings, números, booleanos etc.)
- Referências: coleções de propriedades, representando um heap de memória identificável, que são mutáveis (por exemplo, objetos, arrays, Map etc.)

Digamos que declaramos uma constante, à qual atribuímos uma string:

```tsx
const message = 'hello';
```

Como as strings são primitivas e, portanto, imutáveis, não podemos modificar diretamente esse valor. Só pode ser usado para produzir novos valores:

```tsx
 console.log(message.replace('h', 'sm')); // 'smello' console.log(message); // 'hello' 
```

Apesar de invocar `replace()` no `message`, não estamos modificando sua memória. Estamos apenas criando uma nova string, deixando intacto o conteúdo original do `message`.

Tentar mutar os índices do `message` é um erro operacional por padrão, que lançará um `TypeError` no modo estrito:

```tsx
'use strict';
const message = 'hello';
message[0] = 'j'; // TypeError: 0 is read-only
```

Observe que, se a declaração de `message` usar a palavra-chave `let`, poderemos substituir o valor para o qual ela resolve:

```tsx
let message = 'hello';
message = 'goodbye';
```

É importante destacar que isso não é mutação. Ao invés disso, estamos substituindo um valor imutável por outro.

# Referências Mutáveis

Vamos contrastar o comportamento dos primitivos com as referências. Vamos declarar um objeto com algumas propriedades:

```tsx
const me = {
  name: 'James',
  age: 29,
};
```

Como os objetos JavaScript são mutáveis, podemos alterar suas propriedades existentes e adicionar novas:

```tsx
me.name = 'Rob';
me.isTall = true;
console.log(me); // Object { name: "Rob", age: 29, isTall: true };
```

Diferentemente dos primitivos, os objetos podem ser diretamente alterados sem serem substituídos por uma nova referência. Podemos provar isso compartilhando um único objeto em duas declarações:

```tsx
const me = {
  name: 'James',
  age: 29,
};

const rob = me;

rob.name = 'Rob';

console.log(me); // { name: 'Rob', age: 29 }
```

Os arrays JavaScript, que herdam do `Object.prototype`, também são mutáveis:

```tsx
const names = ['James', 'Sarah', 'Rob'];

names[2] = 'Layla';

console.log(names); // Array(3) [ 'James', 'Sarah', 'Layla' ]
```

# Qual é o problema das referências mutáveis?

Considere que temos um array mutável dos cinco primeiros [números de Fibonacci](https://en.wikipedia.org/wiki/Fibonacci_number):

```tsx
const fibonacci = [1, 2, 3, 5, 8];

log2(fibonacci); // muda cada item, n, por Math.log2(n);
appendFibonacci(fibonacci, 5, 5); // anexa os próximos cinco números Fibonacci no array
```

Esse código pode parecer inofensivo na superfície, mas como o `log2` modifica o array que recebe, nosso array de `fibonacci` não representará mais exclusivamente os números de Fibonacci, como o nome sugere. Ao invés disso, `fibonacci` se tornaria `[0, 1, 1.584962500721156, 2.321928094887362, 3, 13, 21, 34, 55, 89]`. Pode-se, portanto, argumentar que os nomes dessas declarações são semanticamente imprecisos, dificultando o acompanhamento do fluxo do programa.

# Objetos pseudo-imutáveis ​​em JavaScript

Embora os objetos JavaScript sejam mutáveis, podemos tirar proveito de funcionalidades específicas para clonar referências aninhadas, com [spread](https://www.sitepoint.com/shorthand-javascript-techniques/#15spreadoperatorshorthand):

```tsx
const me = {
  name: 'James',
  age: 29,
  address: {
    house: '123',
    street: 'Fake Street',
    town: 'Fakesville',
    country: 'United States',
    zip: 12345,
  },
};

const rob = {
  ...me,
  name: 'Rob',
  address: {
    ...me.address,
    house: '125',
  },
};

console.log(me.name); // 'James'
console.log(rob.name); // 'Rob'
console.log(me === rob); // false
```

A sintaxe _spread_ também é compatível com array:

```tsx
const names = ['James', 'Sarah', 'Rob'];
const newNames = [...names.slice(0, 2), 'Layla'];

console.log(names); // Array(3) [ 'James', 'Sarah', 'Rob' ]
console.log(newNames); // Array(3) [ 'James', 'Sarah', 'Layla' ]
console.log(names === newNames); // false
```

Pensar em imutabilidade ao lidar com tipos de referência pode tornar o comportamento do nosso código mais claro. Revisitando o exemplo anterior do Fibonacci mutável, poderíamos evitar essa mutação copiando `fibonacci` para um novo array:

```tsx
const fibonacci = [1, 2, 3, 5, 8];
const log2Fibonacci = [...fibonacci];

log2(log2Fibonacci);
appendFibonacci(fibonacci, 5, 5);
```

Ao invés de criar cópias no consumidor, seria melhor que o `log2` e o `appendFibonacci` tratassem suas entradas como somente leitura, criando novas saídas baseadas nelas:

```tsx
const PHI = 1.618033988749895;

const log2 = (arr: number[]) => arr.map(n => Math.log2(2));
const fib = (n: number) => (PHI ** n - (-PHI) ** -n) / Math.sqrt(5);

const createFibSequence = (start = 0, length = 5) =>
  new Array(length).fill(0).map((_, i) => fib(start + i + 2));

const fibonacci = [1, 2, 3, 5, 8];
const log2Fibonacci = log2(fibonacci);
const extendedFibSequence = [...fibonacci, ...createFibSequence(5, 5)];
```

Ao escrever nossas funções para retornar novas referências em favor da mutação de suas entradas, o array identificado pela declaração de fibonacci permanece inalterado e seu nome permanece uma fonte válida de identificação. Por fim, esse código é mais determinístico.

# Navegando sobre as rachaduras

Com um pouco de disciplina, podemos agir de acordo com as referências como se elas fossem legíveis, mas o que impede que a mutação aconteça em outro lugar? O que nos impede de introduzir uma declaração para modificar o `fibonacci` em uma parte remota de nosso aplicativo?

```tsx
fibonacci.push(4);
```

O ECMAScript 5 introduziu o `Object.freeze()`, que fornece alguma defesa contra a mutação de objetos:

```tsx
'use strict';

const me = Object.freeze({
  name: 'James',
  age: 29,
  address: {
    // props from earlier example
  },
});

me.name = 'Rob'; // TypeError: 'name' is read-only
me.isTheBest = true; // TypeError: Object is not extensible
```

Infelizmente, proíbe apenas superficialmente a mutação de propriedades e, portanto, os objetos aninhados ainda podem ser alterados:

```tsx
// No TypeErrors will be thrown
me.address.house = '666';
me.address.foo = 'bar';
```

Podemos chamar esse método em todos os objetos em uma árvore em particular, mas isso rapidamente se mostra pesado. 

Como podemos aproveitar os recursos do TypeScript para imutabilidade em tempo de compilação?

# Expressões literais aninhadamente congeladas com asserções const

No TypeScript, podemos usar asserções const, uma extensão das [asserções de tipo](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions), para congelar um tipo aninhado e para somente leitura de uma expressão literal:

```tsx
const sitepoint = {
  name: 'SitePoint',
  isRegistered: true,
  address: {
    line1: 'PO Box 1115',
    town: 'Collingwood',
    region: 'VIC',
    postcode: '3066',
    country: 'Australia',
  },
  contentTags: ['JavaScript', 'HTML', 'CSS', 'React'],
} as const;
```

A anotação `as const` na expressão literal do objeto resulta na computação pelo TypeScript do tipo mais específico e somente leitura que ele conseguir:

```tsx
{
  readonly name: 'SitePoint';
  readonly isRegistered: true;
  readonly address: {
    readonly line1: 'PO Box 1115';
    readonly town: 'Collingwood';
    readonly region: 'VIC';
    readonly postcode: '3066';
    readonly country: 'Australia';
  };
  readonly contentTags: readonly ['JavaScript', 'HTML', 'CSS', 'React'];
}
```

Em outras palavras:

- As primitivas abertas serão reduzidas para tipos literais exatos (por exemplo, `boolean` => `true`)
- Objeto literais terão suas propriedades modificadas com `readonly`
- Arrays literais se tornarão tuplas `readonly` (por exemplo, `string[]` => `['foo', 'bar', 'baz']`)

Tentar adicionar ou substituir quaisquer valores resultará no erro do compilador TypeScript:

```tsx
sitepoint.isCharity = true; // isCharity does not exist on inferred type
sitepoint.address.country = 'United Kingdom'; // Cannot assign to 'country' because it is a read-only property
```

As asserções _const_ resultam em tipos somente leitura, que intrinsecamente impedem a invocação de qualquer método de instância que mude um objeto:

```tsx
sitepoint.contentTags.push('Pascal'); // Property 'push' does not exist on type 'readonly ["JavaScript", "HTML"...] 
```

Naturalmente, o único meio de usar objetos imutáveis ​​para refletir valores diferentes é criar novos objetos a partir deles:

```tsx
const microsoft = {
  ...sitepoint,
  name: 'Microsoft',
} as const;
```

# Parâmetros de função imutáveis

Como as asserções const são apenas um açúcar sintático para digitar uma declaração específica como um conjunto de propriedades somente leitura com valores literais, ainda é possível alterar as referências nos corpos das funções:

```tsx
interface Person {
  name: string;
  address: {
    country: string;
  };
}

const me = {
  name: 'James',
  address: {
    country: 'United Kingdom',
  },
} as const;

const isJames = (person: Person) => {
  person.name = 'Sarah';
  return person.name === 'James';
};

console.log(isJames(me)); // false;
console.log(me.name); // 'Sarah';
```

Podemos resolver isso anotando o parâmetro `person` com `Readonly<Person>`, mas isso afeta apenas as propriedades no nível raiz do objeto:

```tsx
const isJames = (person: Readonly<Person>) => {
  person.name = 'Sarah'; // Cannot assign to 'name' because it is a read-only property.
  person.address.country = 'Australia'; // valid
  return person.name === 'James';
};

console.log(isJames(me)); // false
console.log(me.address.country); // 'Australia'
```

Não há tipos de utilitários embutidos para lidar com imutabilidade anihada, mas, como o TypeScript 3.7 apresenta [melhor suporte para tipos recursivos](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases) ao adiar sua resolução, agora podemos expressar um tipo infinitamente recursivo para indicar propriedades como `readonly` em toda a profundidade de um objeto:

```tsx
type Immutable<T> = {
  readonly [K in keyof T]: Immutable<T[K]>;
};
```

Se descrevêssemos o parâmetro `person` de `isJames()` como `Immutable<Person>`, o TypeScript também nos proibiria de alterar objetos aninhados:

```tsx
const isJames = (person: Immutable<Person>) => {
  person.name = 'Sarah'; // Cannot assign to 'name' because it is a read-only property.
  person.address.country = 'Australia'; // Cannot assign to 'country' because it is a read-only property.
  return person.name === 'James';
};
```

Essa solução também funcionará para arrays profundamente aninhadas:

```tsx
const hasCell = (cells: Immutable<string[][]>) => {
  cells[0][0] = 'no'; // Index signature in type 'readonly string[]' only permits reading.
};
```

Apesar de `Immutable<T>` ser um tipo definido manualmente, [há discussões em andamento para introduzir o DeepReadonly<T> no TypeScript](https://github.com/microsoft/TypeScript/issues/13923), que possui uma semântica mais refinada.

# Um exemplo do mundo real

O [redux](https://redux.js.org/), uma biblioteca de gerenciamento de estado extremamente popular, [exige que o estado seja tratado de forma imutável](https://redux.js.org/faq/immutable-data#what-are-the-benefits-of-immutability) para determinar trivialmente se o estado precisa ser atualizado. Podemos ter interfaces de ação e estado de aplicativo semelhantes a:

```tsx
interface Action {
  type: string;
  name: string;
  isComplete: boolean;
}

interface Todo {
  name: string;
  isComplete: boolean;
}

interface State {
  todos: Todo[];
}
```

Dado que nosso redutor deve retornar uma referência totalmente nova se o estado tiver sido atualizado, podemos digitar o argumento state com `Immutable<State>` para proibir quaisquer modificações:

```tsx
const reducer = (
  state: Immutable<State>,
  action: Immutable<Action>,
): Immutable<State> => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            name: action.name,
            isComplete: false,
          },
        ],
      };

    default:
      return state;
  }
};
```

# Benefícios adicionais da imutabilidade

Ao longo deste artigo, observamos como o tratamento de objetos imutável resulta em um código mais claro e mais determinístico. No entanto, existem algumas vantagens adicionais que vale a pena levantar.

# Detectando alterações com o operador de comparação estrita

Em JavaScript, podemos [usar o operador de comparação estrita](https://www.sitepoint.com/javascript-truthy-falsy/#strictequalitycomparisonswith) (`===`) para determinar se dois objetos compartilham a mesma referência. Considere o nosso redutor no exemplo:

```tsx
const reducer = (
  state: Immutable<State>,
  action: Immutable<TodoAction>,
): Immutable<State> => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        // deeply merge TODOs
      };

    default:
      return state;
  }
};
```

Como criamos apenas uma nova referência se um estado alterado tiver sido calculado, podemos deduzir que a igualdade referencial estrita representa um objeto inalterado:

```tsx
const action = {
  ...addTodoAction,
  type: 'NOOP',
};

const newState = reducer(state, action);
const hasStateChanged = state !== newState;
```

Detectar alterações por igualdade estrita de referência é mais simples do que comparar duas árvores de objetos aninhadas, o que normalmente envolve recursão.

# Memoizando computações por referência

O tratamento de referências e expressões de objetos como um relacionamento individual (ou seja, uma única referência representa um conjunto exato de propriedades e valores), podemos memorizar computações caras por referência. Se quiséssemos adicionar um array contendo os primeiros 2000 números da sequência de Fibonacci, poderíamos usar uma [função de ordem superior](https://www.sitepoint.com/higher-order-functions-javascript/) e um `WeakMap` para armazenar em cache o resultado de uma operação em uma referência específica:

```tsx
const memoise = <TArg extends object, TResult>(func: Function) => {
  const results = new WeakMap<TArg, TResult>();

  return (arg: TArg) =>
    results.has(arg) ? results.get(arg) : results.set(arg, func(arg)).get(arg);
};

const sum = (numbers: number[]) => numbers.reduce((total, x) => total + x, 0);

const memoisedSum = memoise<number[], number>(sum);
const numbers = createFibSequence(0, 2000);

console.log(memoisedSum(numbers)); // Cache miss
console.log(memoisedSum(numbers)); // Cache hit
```

# Imutabilidade não é uma bala de prata

Como todo paradigma de programação, a imutabilidade tem suas desvantagens:

- A cópia de objetos aninhados com a sintaxe spread pode ser verboso, principalmente quando se está alterando apenas um único valor primitivo em uma árvore complexa.
- Criar novas referências resultará em muitas alocações efêmeras de memória, que a coleta de lixo deve, conseqüentemente, dispor. Isso pode atrapalhar o encadeamento principal, embora coletores de lixo modernos como o [Orinoco](https://v8.dev/blog/trash-talk) façam com paralelização.
- Usar tipos imutáveis ​​e afirmações requer disciplina e consenso entre equipes. [Regras de linters](https://github.com/typescript-eslint/typescript-eslint/issues/514) sendo discutidas como um meio de automatizar essas práticas, mas são propostas em estágio inicial.
- Muitas APIs de primeira e de terceiros, como o DOM e as bibliotecas de análise, são modeladas a partir de mutação de objetos. Embora abstrações possam ajudar, a imutabilidade onipresente na Web é impossível.

# Finalizando

O código carregado de mutação pode ter uma intenção opaca e resultar no comportamento inesperado do nosso software. A manipulação da sintaxe moderna do JavaScript pode incentivar os desenvolvedores a operar tipos imutáveis de referência - criando novos objetos a partir de referências existentes, ao invés de modificá-los diretamente - e complementá-los com construções TypeScript para obter imutabilidade em tempo de compilação. Certamente não é uma abordagem infalível, mas com alguma disciplina podemos escrever aplicativos extremamente robustos e previsíveis que, a longo prazo, facilitam nosso trabalho.

# Créditos

- [Compile-time Immutability in TypeScript](https://www.sitepoint.com/compile-time-immutability-in-typescript/), escrito originalmente por [James Wright](https://twitter.com/jamesseanwright)