Tudo sobre o novo operador satisfies no TypeScript

No último beta aberto do TypeScript, os devs mostraram o que virá na versão 4.9 da linguagem. Além de algumas otimizações, que são bastante comuns, na inferência de tipos, vamos ter um novo operador na linguagem, o satisfies. Bora entender um pouco mais como esse operador vai funcionar.

O satisfies

Quando estamos desenvolvendo com TS, costumamos cair em um dilema complicado. A inferência de tipos do TS é muito boa e específica, então é legal termos essa inferência super específica, mas ao mesmo tempo, essa inferência não leva em consideração alguns fatores, como o nome de chaves.

No exemplo que tiramos do próprio anúncio do beta, temos uma ideia muito boa do que isso significa, vamos imaginar o seguinte, temos um tipo que pode ser ou uma string ou uma tupla RGB, ou seja, temos que ter um [number, number, number], podemos fazer dessa forma:

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  bleu: [0, 0, 255]
// ^^^ temos um typo aqui
}

A inferência automática do tipo do TS vai ser que palette é:

{
  red: [255, 0, 0],
  green: "#00ff00",
  bleu: [0, 0, 255]
}

Ou seja, temos um tipo literal. Esses tipos permitem que usemos determinadas funções para cada tipo de dados em certos casos, por exemplo, uma função toUpperCase em green já que é uma string, e um .at(0) no red que é um array:

// Conseguimos usar métodos de array aqui
const redComponent = palette.red.at(0);

// Mas não aqui, porque só podemos usar strings
const greenNormalized = palette.green.toUpperCase();

Só que perdemos a inferência do nome das chaves, veja que temos um typo, deveríamos ter escrito blue e não bleu, mas para corrigir isso teríamos que criar um novo tipo mais genérico e dizer para o TS que aquele tipo é o tipo do objeto, nesse caso o tipo poderia ser:

type Colors = "red" | "green" | "blue"
type RGB = [red: number, green: number, blue: number]

const palette: Record<Colors, string | RGB> = {
  red: [255, 0, 0],
  green: "#00ff00",
  bleu: [0, 0, 255]
}

Isso vai nos dar o erro Object literal may only specify known properties, and 'bleu' does not exist in type 'Record<Colors, string | RGB>'., que é esperado, ou seja, estamos batendo as chaves com o objeto, então podemos corrigi-lo:

const palette: Record<Colors, string | RGB> = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255]
}

Vamos ter um tipo como esse:

{
  red: string | RGB,
  green: string | RGB,
  blue: string | RGB
}

Mas agora a gente vai ter dois erros bem chatos nas duas funções de baixo:

// Property 'at' does not exist on type 'string | RGB'.
const redComponent = palette.red.at(0);

// Property 'toUpperCase' does not exist on type 'string | RGB'.
const greenNormalized = palette.green.toUpperCase();

O primeiro porque não temos .at em strings e o segundo porque não temos .toUpperCase em RGB, e isso só acontece porque, quando dizemos para o TS que aquele objeto é de um determinado tipo, esse tipo vai generalizar o que vai entrar nesse objeto, por exemplo, nesse caso ele está em dúvida se cada chave é uma string ou array de números, e ai não sabemos inferir as duas coisas.

Para resolver esse problema teríamos que fazer uma conversão explícita:

nent = (palette.red as RGB).at(0);
const greenNormalized = (palette.green as string).toUpperCase();

E é para isso que o satisfies existe. Poderíamos manter a especificação mais detalhada, enquanto dizemos que ela deve seguir algum tipo de molde. Por exemplo:

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255]
} satisfies Record<Colors, string | RGB>

Agora o novo tipo de palette vai ser:

{
    red: [number, number, number];
    green: string;
    blue: [number, number, number];
}

Veja como agora estamos usando o poder da inferência específica com o poder da inferência de tipos declarada para poder tirar o máximo proveito do TypeScript. Dessa forma vamos poder usar funções que são string dentro de chaves que são string e funções de arrays dentro de chaves numéricas.

Conclusão

Essa é, sem dúvida uma das melhores inclusões do superset nos últimos tempos. Acredito que a principal recomendação a partir de agora seja escrever todos os seus tipos sem nenhum tipo de asserção manual, e utilizar o satisfies para poder dizer a qual objeto ele deve pertencer, dessa forma conseguimos manter o melhor de todos os mundos.

Para mais exemplos, dê uma olhada na issue que propôs essa funcionalidade.