Esse artigo é um complemento ao meu vídeo com o mesmo tema! Veja ele aqui:

Se você está codando em TypeScript, provavelmente já se deparou com um tipo muito estranho, o famoso const as const, algo assim:

const foo = {
  bar: 'string'
} as const

Inclusive, a gente até chegou a falar dele quando falamos sobre enums aqui no blog. Você pode imaginar que, por ter um as, isso deve ser algum tipo de type casting, ou seja, estamos trocando um tipo pelo outro e forçando o TS a aceitar isso, o que é uma má prática. Mas não!

Este tipo de técnica é chamada de const assertions e, como o nome diz, é uma asserção, ou seja, estamos dando mais informações sobre um tipo para o TypeScript. Mas o que estamos dizendo para ele?

Objetos e arrays

Esse tipo de asserção é geralmente utilizado com objetos e arrays, e também é a forma mais simples de entender o conceito. Mas imagine que você tenha esse objeto:

const args = [1, 2]
const sum = (a, b) => a+b

Tudo que o TS sabe é que args é um array de números então ele vai tipar como number[], o que é aceitável, porque você poderia, por exemplo, fazer um array.push(0) e ele aceitaria sem problemas, na verdade, na maioria dos casos isso é o que acontece. Mas tem um caso que não.

Existem várias funções no JavaScript que precisam de um número exato de argumentos, um exemplo é a função Math.atan2 que recebe exatamente 2 parâmetros. Outro exemplo é a nossa função de soma. Então esse código não funciona:

const args = [1, 2]
const sum = (a, b) => a+b
sum(...args)

Porque o TS não consegue inferir a quantidade de parâmetros que args tem, e nem o total de itens presentes no array, justamente porque podemos adicionar, remover ou modificar o array da forma que quisermos.

Se a gente quiser dizer que esse array é imutável, uma constante, temos que usar uma const assertion

const args = [1, 2] as const
const sum = (a, b) => a+b
sum(...args)

Neste caso, args agora é tipado como readonly [1, 2], ou seja, não podemos modificar o array, e ele tem exatamente dois elementos, 1 e 2. Portanto podemos passar para a função porque dizemos para o TS que ele sempre vai ter dois elementos.

Vem aprender comigo!

Quer aprender mais sobre criptografia e boas práticas com #TypeScript?

Se inscreva na Formação TS!

O mesmo é válido para objetos, se passarmos um objeto qualquer como este:

const obj = {
  foo: 1,
  bar: 'formacaots.com.br'
}

O TypeScript vai simplesmente tipá-lo como um objeto dessa forma:

const obj: {
    foo: number;
    bar: string;
}

E podemos adicionar ou remover chaves, além de poder passar qualquer string e qualquer número, ou até mesmo mudar o tipo do objeto, mas se usarmos

const obj = {
  foo: 1,
  bar: 'formacaots.com.br'
} as const

Nossa tipagem muda para:

const obj: {
    readonly foo: 1;
    readonly bar: "formacaots.com.br";
}

Veja que agora ele não é só imutável, mas também tem os tipos literais como chaves. E você pode estar se perguntando: "E o Object.freeze? Não faz a mesma coisa? Não é até mais seguro por que está em runtime?"

Existe uma diferença fundamental entre o Object.freeze e o as const, enquanto o Object.freeze vai sim deixar o objeto imutável em tempo de execução, ele só faz isso para o primeiro nível de chaves, então chaves internas e compostas como:

const foo = {
  bar: {
    baz: 1
  }
}

Não funcionam, se você usar Object.freeze(foo), isso garante que bar não pode sofrer a alteração para outro objeto, mas foo.bar.baz pode ser alterado normalmente. Já o as const faria o objeto inteiro ser imutável.

Conclusão

const assertions fazem três coisas:

  • Em tipos primitivos (string, number, boolean, etc), vai remover a possibilidade de type widening, ou seja, um 'foo' virar string.
  • Propriedades de objetos se tornam readonly
  • Arrays se tornam tuplas imutáveis (readonly)

Você pode inclusive usar as const em retornos de função, para fazer com que o tipo inferido da função seja sempre inferido como o tipo mais estático possível:

function foo () {
  return [1, 2] as const // retorna uma tupla
}