FNSP Web Page

Type guards: ensinar o compilador a acompanhar o raciocínio

O TypeScript segue raciocínios simples sozinho. Um if que elimina o null faz com que, da chaveta para dentro, o valor deixe de ser nulo — e isso chama-se estreitamento.

function saudar(nome: string | null) {
  if (nome === null) return;
  // aqui dentro, nome é string — sem ninguém o dizer
  return nome.toUpperCase();
}

O problema aparece quando a verificação deixa de caber numa expressão, e passa para uma função. Aí o compilador perde o fio.

1. A função que parte o raciocínio

function temValor(x: string | null) {
  return x !== null;
}

function saudar(nome: string | null) {
  if (temValor(nome)) {
    return nome.toUpperCase();  // erro: nome ainda pode ser null
  }
}

E tem razão em queixar-se: pelo que a assinatura diz, aquela função devolve um boolean — não diz sobre o quê. Qualquer função que devolva true serviria.

2. A forma de o dizer

Troca-se o tipo de retorno por um predicado:

function temValor(x: string | null): x is string {
  return x !== null;
}

Aquele x is string é uma promessa ao compilador: «quando eu devolver verdadeiro, podes contar que o argumento é desta forma». Com ela, o estreitamento volta a funcionar do outro lado da chamada.

O caso onde isto rende mais é o que este site usa para limpar uma lista:

const usados = tags
  .map((t) => t.hue)
  .filter((h): h is number => h !== null);
// usados: number[]  — e não (number | null)[]

Sem o predicado, o filter devolveria (number | null)[]. O compilador sabe que filtrámos, mas não sabe porquê — e com o predicado passa a saber.

3. A responsabilidade que vem junto

Um predicado não é verificado. Isto compila sem um aviso:

function eNumero(x: unknown): x is number {
  return typeof x === 'string';   // mentira, e ninguém repara
}

É o mesmo género de poder que um as dá, mas concentrado num sítio só. É isso que o torna aceitável: a afirmação fica numa função pequena, com nome, que se pode testar — em vez de espalhada por vinte sítios.

4. Antes de escrever um, ver se é preciso

Muitas verificações já estreitam sozinhas, e o predicado seria trabalho a mais:

typeof x === 'string'          // estreita
Array.isArray(x)               // estreita
erro instanceof Prisma.Error   // estreita
'codigo' in resposta           // estreita

O último é o mais esquecido, e resolve boa parte dos casos de uniões de objetos sem necessidade de nada mais.

5. O caso em que isto brilha mesmo

Uma união discriminada com um campo que diz qual é qual:

type Resultado =
  | { estado: 'ok'; dados: string[] }
  | { estado: 'erro'; mensagem: string };

function mostrar(r: Resultado) {
  if (r.estado === 'erro') {
    return r.mensagem;     // só este ramo tem mensagem
  }
  return r.dados.length;   // e só este tem dados
}

Aqui não é preciso predicado nenhum: o compilador segue o campo estado e sabe, em cada ramo, com qual das formas está a lidar. É o padrão que torna metade dos predicados desnecessários — e está mais desenvolvido no artigo sobre uniões discriminadas.

O resumo

Um predicado serve para levar uma verificação para fora sem o compilador perder o fio. Antes de escrever um, vale a pena ver se o typeof, o in ou um campo discriminante já não fazem o trabalho — porque esses o compilador verifica, e um predicado é uma promessa que ninguém confere.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.