Os tipos predicativos são uma interessante característica sintática em TypeScript. Apesar de aparecerem no mesmo local que as anotações de tipo de retorno, eles se parecem mais com frases afirmativas curtas do que com as anotações típicas. Isto dá a você maior controle sobre a verificação de tipos.

Com a versão 5.5 do TypeScript, trabalhar com tipos predicativos tornou-se mais intuitivo, pois agora pode inferir automaticamente em muitos casos. Mas se você estiver navegando em bases de código um pouco mais antigas, é provável que encontre tipos predicativos escritos à mão com maior frequência.

Neste artigo, exploraremos brevemente o que são tipos predicativos e por que são úteis. Vamos começar olhando para o problema que eles resolvem.

O Problema

A melhor maneira que acho para entender a utilidade dos tipos predicativos é notando os problemas que surgem quando não temos eles:

function isString(value: unknown): boolean {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

Aqui, o tipo de retorno de isString está definido como boolean, e usamos-lo em uma função chamada padLeft para adicionar recuo à esquerda de uma string de entrada. O padding pode ser qualquer string dada ou um número especificado de caracteres de espaço.

Se você está se perguntando por que eu codifiquei o tipo de retorno como boolean. Isso serve para ilustrar o problema. Se você não adicionar nenhuma anotação de tipo de retorno e usar a versão mais recente do TypeScript, você não notará nenhum problema aqui. Por agora, tenha paciência – vamos discutir as diferenças relacionadas à versão em breve.

A função funcionará suavemente em tempo de execução, mas o TypeScript não pode realizar qualquer refinamento de tipo com isString. Como resultado, o tipo de padding permanece string | number tanto dentro quanto fora da instrução if. Isso leva a um conflito com a expectativa de repeat para seu primeiro argumento, causando o erro de tipo.

A Solução: Introduzindo Predicados de Tipo

Mesmo se você não estiver familiarizado com o termo predicado, provavelmente já os usou antes. Predicados em programação são simplesmente funções que retornam um booleano para responder a uma pergunta de sim/não. Vários métodos integrados de array do JavaScript, como filter, find, every, e some, usam predicados para auxiliar na tomada de decisão.

Predicados de tipo são uma forma de tornar os predicados mais úteis para o refinamento de tipos. Podemos corrigir o problema usando um predicado de tipo como o tipo de retorno:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

Aqui o predicado de tipo é value is string. Ele está dizendo três coisas:

  • A função é um predicado. Portanto, o TypeScript mostrará um erro se você tentar retornar qualquer coisa diferente de um valor Booleano.

  • Se retornar true, então value é do tipo string.

  • Se retornar false, então value não é do tipo string.

Predicados de tipo permitem criar guias de tipo personalizadas. Guias de tipo são verificações lógicas que permitem refinamentos de tipos para tipos mais específicos, ou seja, encolhê-los. Portanto, a função acima é também uma guia de tipo personalizada.

Aqui está o código completo:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input;
                 //   ^
                 // number
}

Aqui, TypeScript corretamente encolhe o tipo de padding dentro do laço if e fora dele.

Agora vamos olhar rápido como os predicados de tipo funcionavam antes do TypeScript 5.5 e o que esta versão melhorou.

Predicados de tipo Antes do TypeScript 5.5

No nosso exemplo anterior, se não especificarmos nenhum tipo de retorno, será inferido como boolean:

function isString(value: unknown) {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

Como resultado, temos o mesmo erro que quando escrevemos manualmente o tipo de retorno boolean. Aqui está o link para o TypeScript playground para o fragmento de código acima. Vá e passe o mouse sobre as funções ou variáveis para uma melhor sensação dos tipos. Então veja como a escrita da predicada de tipo resolve o problema.

Se não especificarmos a predicada de tipo, usar métodos como filter também pode resultar em detecção de tipo incorreta:

function isString(value: unknown) {
  return typeof value === "string";
}

const numsOrStrings = [1, 'hello', 2, 'world'];
//      ^
//    strings: (string | number)[]

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: (string | number)[]

Agora vamos ver como o TypeScript 5.5 melhora a situação.

Predicadas de Tipo Após o TypeScript 5.5

Uma das principais funcionalidades do TypeScript 5.5 é que ele pode inferir predicadas de tipo分析ando o corpo da função. Portanto, se você estiver usando TypeScript 5.5 ou posterior, você não precisa escrever a predicada de tipo como o tipo de retorno de isString. O TypeScript faz isso por você, e o código como o que você vê no exemplo abaixo funciona perfeitamente bem:

function isString(value: unknown) {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input; // Ops erro de tipo aqui
                 //   ^
                 // número
}

const numsOrStrings = [1, 'hello', 2, 'world'];

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: string[]

const numbers = numsOrStrings.filter((v) => !isString(v));
//      ^
//    números: number[]

Ainda não encontrei uma situação em que estou insatisfeito com a inferência automática de predicados de tipo. Se você encontrar alguma, sempre pode escrever a sua própria manualmente.

Estudo Adicional

Neste artigo, exploramos brevemente os predicados de tipo no TypeScript. Se você estiver interessado em aprender mais e entender os casos extremos, aqui estão os guias oficiais:

Obrigado por ler! Até a próxima vez!

O fundo da imagem de capa é de Mona Eendra no Unsplash