Funções de asserção ou Guards de asserção - TypeScript Narrowing #5

TypeScript Narrowing #5

Bem-vindo ao quinto artigo da nossa série sobre narrowing em TypeScript! Leia aos anteriores, caso ainda não tenha lido. Os links estão na descrição.

Neste artigo, vou abordar assertion functions, também conhecidas como assertion guards.

Sou Lucas Paganini, e neste site, lançamos tutoriais de desenvolvimento web. Inscreva-se na newsletter se estiver interessado nisso.

LinkFunções de Asserção vs Type Guards

A razão pela qual assertion functions também são conhecidas como assertion guards, é devida a sua semelhança com type guards.

No nosso type guards para strings, retornamos true se o argumento fornecido é uma string e false se não for.

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

Se quiséssemos uma assertion function em vez de um type guard, em vez de retornar true ou false, nossa função retornaria ou lançaria (throw). Se for uma string, ela retorna, se não, ela lança um erro.

TypeScript
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}

Se você chamar uma função que lança um erro se o seu valor não for uma string, então todo o código que vier depois dela só será executado se o valor for uma string, portanto, o TypeScript estreita o nosso tipo a string.

TypeScript
const x = 'abc' as string | number;
x; // <- x: `string | number`

assertIsString(x);
x; // <- x: `string`

Para abstrair esta explicação: TypeScript usa control flow analysis para restringir nosso tipo ao que foi afirmado. Nesse caso, afirmamos que o valor é uma string.

TypeScript
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}

Falamos sobre control flow analysis no segundo vídeo desta série, o link está nas referências.

LinkSaídas Antecipadas

Agora, quando e por que iríamos querer usar uma assertion function em vez de um type guard?

Bem, o cenário mais popular para assertion functions é validação de dados.

Suponha que você tem um servidor NodeJS e você está escrevendo um handler para criação de usuários.

TypeScript
/** Expected body for POST /api/users */
interface CreatableUser {
  name: string;
  email: string;
  password: string;
}

A primeira coisa que você deve fazer em seus handlers é validar os dados. Se algum dos campos estiver faltando ou for inválido, você vai querer lançar um erro.

TypeScript
function assertIsCreatableUser(value: unknown): asserts value is CreatableUser {
  if (typeof value !== 'object') throw Error('Creatable user is not an object');
  if (value === null) throw Error('Creatable user is null');

  assertHasProps(['name', 'email', 'password'], value);
  assertIsName(value.name);
  assertIsEmail(value.email);
  assertIsPassword(value.password);
}

Quando você tem condições para verificar logo no início do seu código e você se recusa a rodar se essas condições forem inválidas, isso é chamado "early exits" e é o cenário perfeito para assertion functions!

TypeScript
const userCreationHandler = (req: Request, res: Response): void => {
  try {
    // Validate the data before anything
    const data = req.body
    assertIsCreatableUser(data)

    // Data is valid, create the user
    ...
  } catch (err) {
    // Data is invalid, respond with 400 Bad Request
    const errorMessage =
      err instanceof Error
        ? err.message
        : "Unknown error"
    res.status(400).json({ errors: [{ message: errorMessage }] })
  }
}

Se você quiser saber mais sobre early exits, tenho um vídeo de um minuto explicando esse conceito. Vou deixar o link para ele nas referências.

TypeScript
/** Non empty string between 3 and 256 chars */
type Name = string;

function assertIsName(value: unknown): asserts value is Name {
  if (typeof value !== 'string') throw Error('Name is not a string');
  if (value.trim() === '') throw Error('Name is empty');
  if (value.length < 3) throw Error('Name is too short');
  if (value.length > 256) throw Error('Name is too long');
}

LinkProblemas com Análise de Fluxo de Controle

Talvez você tenha percebido que estou usando uma function declaration em vez de uma function expression. Existe uma razão para isso.

A propósito, saber as diferenças entre function declarations e function expressions é muito importante. Tenho certeza que a maioria de vocês já sabe disso, mas se não souber, está tudo bem. Eu fiz um vídeo de um minuto explicando exatamente isso, e estou deixando o link para ele nas referências.

Então, de volta às assertion functions. Estou usando function declarations porque TypeScript tem problemas para reconhecer assertion functions durante control flow analysis se elas forem escritos como function expressions.

As function declarations funcionam porque são hoisted, então seus types são declarados previamente e o TypeScript gosta disso.

Pessoalmente, julgo que isso é um bug. Mas não sei se vão consertar isso. Atualmente, eles dizem que está funcionando conforme o esperado.

Para contornar esse problema, encontrei duas alternativas:

  1. Usar function declarations.
TypeScript
// Alternative 1: Functions Declaration

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') throw Error('value is not a string');
}
  1. Usar function expressions com types predefinidos
TypeScript
// Alternative 2: Function Expressions with Predefined Types

// Predefined type
type AssertIsString = (value: unknown) => asserts value is string;

// Function expression with predefined type
const assertIsString: AssertIsString = (value) => {
  if (typeof value !== 'string') throw Error('value is not a string');
};

LinkFunction Expressions with Predefined Types

Eu prefiro function expressions, então vou com a segunda alternativa.

Para isso, em vez de definir a assinatura da nossa função com a sua implementação:

TypeScript
// DON'T: Signature with implementation
const assertIsString = (value: unknown): asserts value is string => {
  if (typeof value !== 'string') throw Error('value is not a string');
};

Teremos que definir sua assinatura como um type isolado e moldar nossa função a esse type.

TypeScript
// Predefined type
type AssertIsString = (value: unknown) => asserts value is string;

// Function expression with predefined type
const assertIsString: AssertIsString = (value) => {
  if (typeof value !== 'string') throw Error('value is not a string');
};

É assim que ficaria se quiséssemos usar function expressions para nossa função assertIsName:

TypeScript
// Predefined type
type AssertIsName = (value: unknown) => asserts value is Name;

// Function expression with predefined type
const assertIsName: AssertIsName = (value) => {
  if (typeof value !== 'string') throw Error('Name is not a string');
  if (value.trim() === '') throw Error('Name is empty');
  if (value.length < 3) throw Error('Name is too short');
  if (value.length > 256) throw Error('Name is too long');
};

E nós também estamos usando uma função assertHasProps para verificar se nosso objeto tem as propriedades que esperamos. Talvez você esteja curioso, então estou mostrando-a também, pois ela possui uma assinatura interessante:

TypeScript
// Predefined type
type AssertHasProps = <Prop extends string>(
  props: ReadonlyArray<Prop>,
  value: object
) => asserts value is Record<Prop, unknown>;

// Function expression with predefined type
const assertHasProps: AssertHasProps = (props, value) => {
  // Only objects have properties
  if (typeof value !== 'object') throw Error(`Value is not an object`);

  // Make sure it's not null
  if (value === null) {
    throw Error('Value is null');
  }

  // Check if it has the expected properties
  for (const prop of props)
    if (prop in value === false) throw Error(`Value doesn't have .${prop}`);
};

Também estou deixando um link para os issues do Github e PRs relacionadas a isso se você quiser saber mais.

LinkAsserções sem um predicado de tipo

Antes de encerrarmos, quero te mostrar uma assinatura diferente para assertion functions:

TypeScript
type Assert = (condition: unknown) => asserts condition;
const assert: Assert = (condition) => {
  if (condition == false) throw 'Invalid assertion';
};

Estranho, certo? Não há type predicates, o que raios estamos afirmando?

Esta assinatura significa que a condição para verificarmos já é um type guard. Por exemplo, você pode passar uma expressão typeof e o TypeScript fará o narrowing respectivo.

TypeScript
const x = 'abc' as string | number;
x; // <- x: `string | number`

assert(typeof x === 'string');
x; // <- x: `string`

LinkConclusão

Assertion functions são legais, certo? Só não estamos acostumados com elas ainda.

As referências estão abaixo. Se você gostou do conteúdo, sabe o que fazer.

E se sua empresa está procurando por desenvolvedores web remotos, considere entrar em contato comigo e com a minha equipe em lucaspaganini.com.

Este não é o último artigo desta série. Tem mais por vir! Então...

Até lá, tenha um ótimo dia, e te vejo no próximo artigo!

LinkConteúdo Relacionado

  1. 1m JS: Early Exits
  2. TypeScript Narrowing pt. 1 - 8

LinkReferências

  1. Assertion functions Documentação do TypeScript
  2. Pull Request - Assertion Functions Repositório do TypeScript no GitHub
  3. Pull Request - Mensagem de erro para Assertion Functions que não poderiam ser analisadas por Control Flow Repositório do TypeScript no GitHub
  4. Issue - Assertion Functions e Function Expressions: Repositório do TypeScript no GitHub
  5. Exemplos de códigos Lucas Paganini

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