Type Guards Personalizados- TypeScript Narrowing #3

TypeScript Narrowing #3

Ei! Bem-vindo a outro artigo na nossa série de Narrowing em TypeScript. Neste artigo, vou explicar:

  1. Type predicates
  2. Como criar seus próprios guards
  3. Como criar um guard por exclusão

Este é o terceiro artigo da nossa série, se você não assistiu os anteriores, eu recomendo fortemente que você leia, eles abordam fundamentos sólidos para narrowing. Vou deixar um link para eles nas referências.

Sou Lucas Paganini, e neste site, lançamos tutoriais de desenvolvimento web. Se você está interessado nisso, deixe um like e inscreva-se.

LinkType Predicates

No último artigo, exploramos os operadores type guard fundamentais. Agora, eu gostaria de te mostrar funções de type guard.

Por exemplo, se você precisa verificar se uma variável chamada value é uma string, você pode usar o operador typeof. Mas o que você também pode fazer, é criar uma função chamada isString() que recebe um argumento e retorna true se o argumento fornecido for uma string.

TypeScript
const isString = (value: any): boolean => typeof value === 'string';

Você se lembra da nossa função formatErrorMessage() do último artigo?

TypeScript
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (typeof value === 'string') {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  if (value instanceof Error) {
    return prefix + value.message;
  }

  // We will never reach here
  throw new Error(`Invalid value type`);
};

interface Warning {
  text: string;
}

Vamos remover o operador typeof dela e usar isString() no lugar.

TypeScript
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (isString(value)) {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  if (value instanceof Error) {
    return prefix + value.message;
  }

  // We will never reach here
  throw new Error(`Invalid value type`);
};

interface Warning {
  text: string;
}

É o mesmo código, somente isolamos o guard em uma função, certo? Não. O código quebra. TypeScript não está estreitando o tipo para string, o guard não está funcionando.

O problema é o seguinte: isString() está retornando um boolean, e nós sabemos o que esse boolean significa.

TypeScript
const isString = (value: any): boolean => typeof value === 'string';

Significa que o argumento é uma string. Mas o TypeScript não sabe o que esse boolean significa, então precisamos informá-lo.

Em vez de dizer que nossa função retorna um boolean, precisamos dizer que nossa função retorna a resposta à pergunta: "este argumento é uma string?".

Dado que o nome do nosso argumento é value, fazemos isso com a seguinte sintaxe: value is string.

TypeScript
const isString = (value: any): value is string => typeof value === 'string';

Agora o TypeScript entende que isString() é um type guard e nossa função formatErrorMessage() compila corretamente.

O retorno da nossa função isString() não é apenas um boolean, é um "Type Predicate".

Então, para fazer um type guard personalizado, basta criarmos uma função que retorna um type predicate.

Todos os type predicates assumem a forma de { parameter } é { Type }.

LinkO Tipo unknown

Uma dica rápida antes de continuarmos:

Em vez de usar o tipo any em nosso type guard personalizado, nosso código seria mais seguro se usássemos o tipo unknown.

TypeScript
const isString = (value: unknown): value is string => typeof value === 'string';

Fiz um vídeo de um minuto explicando as diferenças entre any e unknown, o link está nas referências

LinkGuards personalizados

Vamos exercitar o nosso conhecimento convertendo todas as verificações em nossa função formatErrorMessage() para type guards personalizados.

Já temos uma guard para strings, agora precisamos de guards para Warning, Error e Falsy.

Error Guard

O guard para Error é bem simples, apenas isolamos a verificação com o instanceof em uma função.

TypeScript
const isError = (value: unknown): value is Error => value instanceof Error;

Warning Guard

Mas o guard para Warning, por outro lado, não é tão simples.

TypeScript nos permitiu usar o operador in porque havia uma quantidade limitada de tipos que o nosso parâmetro value poderia ser, e todos eram objetos.

TypeScript
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (!value) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (isString(value)) {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if ('text' in value) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  if (isError(value)) {
    return prefix + value.message;
  }

  // We will never reach here
  throw new Error(`Invalid value type`);
};

interface Warning {
  text: string;
}

Mas se criarmos uma função e dissermos que nosso parâmetro é unknown, então ele pode ser qualquer coisa, incluindo tipos primitivos. E isso causaria um Error porque só podemos usar o operador in em objetos.

TypeScript
interface Warning {
  text: string;
}

const isWarning = (value: unknown): value is Warning => 'text' in value; // Compilation error

A solução é garantir que o nosso parâmetro seja um objeto válido antes de usar o operador in. E também precisamos ter certeza de que ele não é null.

TypeScript
interface Warning {
  text: string;
}

const isWarning = (value: unknown): value is Warning =>
  typeof value === 'object' && value !== null && 'text' in value;

Falsy Guard

Para o guard de valores falsy, primeiro precisamos definir um tipo com valores considerados falsy.

TypeScript
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;

Não estou incluindo NaN aqui porque não há um tipo NaN em TypeScript.

TypeScript
type Falsy = false | 0 | -0 | 0n | '' | null | undefined | ~~NaN~~;

NaN é do tipo number e nem todos os números são falsy, por isso não estamos lidando com NaN.

TypeScript
typeof NaN;
//=> number

Existe uma proposta para adicionar NaN como um tipo e também integer, float e Infinity. Eu considero uma boa proposta, seria muito útil ter esses tipos disponíveis.

TypeScript
// Proposal
type number = integer | float | NaN | Infinity;

Vou deixar um link para a proposta nas referências.

De qualquer forma, agora que temos nosso tipo Falsy, podemos criar um guard para valores falsy.

Lembre-se, um valor é falsy se for considerado false quando convertido para boolean. Então, para verificar se nosso valor é falsy, podemos usar igualdade abstrata para ver se ele é convertida para false.

TypeScript
type Falsy = false | 0 | -0 | 0n | '' | null | undefined;

const isFalsy = (value: unknown): value is Falsy => value == false;

LinkformatErrorMessage() com Guards Personalizados

É isso, agora temos todos os guards personalizados que precisamos para nossa função formatErrorMessage()

TypeScript
// FUNCTION
const formatErrorMessage = (
  value: null | undefined | string | Error | Warning
): string => {
  const prefix = 'Error: ';

  // If it's falsy (null, undefined, empty string), return "Unknown" with the prefix
  if (isFalsy(value)) {
    return prefix + 'Unknown';
  }

  // If it's a string, return the string with the prefix
  if (isString(value)) {
    return prefix + value;
  }

  // If it's a Warning, return the Warning.text with the prefix
  if (isWarning(value)) {
    return prefix + value.text;
  }

  // If it's an Error, return the Error.message with the prefix
  if (isError(value)) {
    return prefix + value.message;
  }

  // We will never reach here
  throw new Error(`Invalid value type`);
};
TypeScript
// GUARDS
const isString = (value: unknown): value is string => typeof value === 'string';

const isError = (value: unknown): value is Error => value instanceof Error;

interface Warning {
  text: string;
}

const isWarning = (value: unknown): value is Warning =>
  typeof value === 'object' && value !== null && 'text' in value;

type Falsy = false | 0 | -0 | 0n | '' | null | undefined;

const isFalsy = (value: unknown): value is Falsy => value == false;

LinkBONUS: Narrowing por Exclusão

Antes de encerrarmos, eu quero te mostrar uma coisa.

Existe uma lista limitada de valores falsy, certo?

markdown
1. `false`
2. `0` `-0` `0n` representações de zero
3. ```` `""` `''` string vazio
4. `null`
5. `undefined`
6. `NaN` not a number

Mas os valores truthy, por outro lado, são infinitos. Todos os valores que não são falsy são truthy.

Então, como faríamos um guard para valores truthy?

Truthy Guard

O truque é excluir os types falsy.

Ao invés de verificar se nosso valor é truthy, verificamos se _não_ é falsy.

TypeScript
type Truthy<T> = Exclude<T, Falsy>;

const isTruthy = <T extends unknown>(value: T): value is Truthy<T> =>
  value == true;

// Test
const x = 'abc' as null | string | 0;
if (isTruthy(x)) {
  x.trim(); // `x: string`
}

Eu uso muito esse truque e veremos ele novamente em vídeos futuros.

LinkConclusão

Referências e outros links estão abaixo.

Se ainda não o fez, deixe seu like e inscreva-se. Isso nos ajuda a crescer, o que resulta em mais conteúdo gratuito para você. Todos ganham.

E se sua empresa está procurando desenvolvedores web remotos, eu e a minha equipe estamos disponíveis para novos projetos. Você pode entrar em contato conosco em lucaspaganini.com.

Tenha um ótimo dia e nos vemos em breve.

LinkConteúdo Relacionado

  1. 1min JS - Falsy and Truthy
  2. 1min TS - Unknown vs Any
  3. TypeScript Narrowing Series
  4. TypeScript Narrowing Parte 1 - O que é um Type Guard
  5. TypeScript Narrowing Parte 2 - Type Guard Operators

LinkReferências

  1. Narrowing Documentação do TypeScript
  2. Type predicates Documentação do TypeScript
  3. O operador in MDN Docs
  4. TypeScript proposal to add NaN as a type Repositório do TypeScript no GitHub

Assine a nossa Newsletter e seja avisado quando eu lançar um curso, postar um vídeo ou escrever um artigo.

Campo obrigatório
Campo obrigatório