Os tipos predicativos são uma característica sintáctica interessante no TypeScript. Enquanto eles aparecem no mesmo local que as anotações de tipo de retorno, parecem mais como frases afirmativas curtas do que anotações típicas, o que dá maior controle sobre a verificação de tipos.

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

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

O Problema

A melhor maneira para entender a utilidade dos tipos predicativos, acho eu, é 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 preenchimento à esquerda de uma string de entrada. O padding pode ser uma string dada ou um número especificado de caracteres de espaço.

Você deve estar se perguntando por que defini o tipo de retorno como boolean. Isso é 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, não notará nenhum problema aqui. Por enquanto, fique comigo – discutiremos as diferenças relacionadas à versão em breve.

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

A Solução: Entre Predicados de Tipo

Mesmo que você não esteja familiarizado com o termo predicado, provavelmente já os usou. 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 de array embutidos do JavaScript, como filter, find, every e some, usam predicados para auxiliar na tomada de decisões.

Predicados de tipo são uma maneira de tornar os predicados mais úteis para estreitamento de tipo. Podemos resolver o problema usando um predicado de tipo como tipo de retorno:

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

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

  • A função é um predicado. Então, o TypeScript mostrará um erro se você tentar retornar qualquer coisa além 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 que você crie 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 if statement e fora dele.

Agora vamos brevemente ver como os predicados de tipo funcionavam antes do TypeScript 5.5 e o que essa 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 do TypeScript playground para o trecho de código acima. Vá e passe o mouse sobre as funções ou variáveis para ter uma melhor sensação dos tipos. Então veja como escrever o predicado de tipo resolve o problema.

Se não especificarmos o predicado 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.

Predicados de Tipo Após o TypeScript 5.5

Um dos principais recursos do TypeScript 5.5 é que ele pode inferir predicados de tipo analisando o corpo da função. Portanto, se você estiver usando o TypeScript 5.5 ou posterior, não é necessário escrever o predicado de tipo como o tipo de retorno de isString. O TypeScript faz isso por você, e códigos como o exemplo abaixo funcionam perfeitamente:

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; // Opps type error here
                 //   ^
                 // number
}

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

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

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

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

Estudo Avançado

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

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

O fundo da capa da foto é de Mona Eendra no Unsplash.