Guardas de Ordem Superior (Funções) - TypeScript Narrowing #6

TypeScript Narrowing #6

Oooooff, já é a parte 6! Me pergunto quantos de vocês estão lendo aqui desde o início.

Hoje vamos pegar um padrão do mundo da programação funcional conhecido como "Higher-order Functions" e usar isso para criar funções que recebem funções e retornam novas funções.

TypeScript
function f1() {}
TypeScript
function f1(f2: Function) {}

Mas não vamos parar aí. Não vamos apenas retornar novas funções, vamos retornar novos type guards personalizados! Então, estou chamando essas funções de criação de guardas de "Guardas de Ordem Superior".

TypeScript
const makeIsNot = fn => ✨magic✨

Como você vai descobrir em breve, isso vai abrir a porta para novas possibilidades de reutilizar nosso código.

TypeScript
const makeIsNot = fn => ✨magic✨

const isNotString = makeIsNot(isString)

let aaa = 'abc' as  string | number | boolean
if (isNotString(aaa)) {
  aaa // <- aaa: number | boolean
} else {
  aaa // <- aaa: string
}

Eu sou o Lucas Paganini, e nesse blog, nós lançamos tutoriais de desenvolvimento web. Se inscreva se você tiver interesse na área.

LinkFunções de Ordem Superior

A descrição teórica de uma função de ordem superior é um trava-línguas: uma função, que recebe uma função, e retorna outra função. Então deixa eu te mostrar na prática e você vai ver que não é assim tão complexo quanto parece.

Vamos dizer que temos um monte de type guards personalizados, e agora nós queremos versões invertidas deles.

  • Nós já temos isString, e agora queremos isNotString.
  • Já temos isNumber, e agora queremos isNotNumber.
  • Você pegou a ideia...

Fizemos algo muito parecido no final do nosso terceiro artigo, quando nós escrevemos um guarda para valores truthy que funcionava excluindo valores falsy.

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

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

// Test
let x: null | string | 0;
if (isTruthy(x)) {
  x.trim(); // <- x: string
}

Nós podemos aplicar a mesma técnica para criar nossos type guards invertidos. Eles serão da seguinte forma:

TypeScript
const isNotString = <V extends unknown>(
  value: V
): value is Exclude<V, string> => isString(value) === false;

const isNotNumber = <V extends unknown>(
  value: V
): value is Exclude<V, number> => isNumber(value) === false;

Mas escrever esses type guards invertidos manualmente é entediante e repetitivo. Aposto que você consegue ver um padrão neles: tudo o que precisamos para criar um guarda invertido, é o type guard personalizado que vai ser invertido.

Em outras palavras: a única diferença entre isNotString e isNotNumber, é que um está usando o guarda isStringe o outro está usando o guarda isNumber.

LinkGuardas de Ordem Superior

Será que podemos parar de nos repetir e criar uma função que aceite um type guard como um argumento e retorne a versão invertida do type guard oferecido?

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);

Sim, nós podemos! Vamos criar isso agora!

Eu tenho uma convenção pessoal de prefixar funções com a palavra make quando elas retornam novas funções. Então faz sentido chamar nossa função de makeIsNot, já que ela retorna a versão is not de uma type guard.

LinkImplementação de makeIsNot

A implementação da função já é um pouco complicada por si só, então vamos dar uma navegada nela antes de entrar na sua assinatura.

Usemos, como exemplo, isNotString:

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);

Chamar makeIsNot com isString, retorna uma função que recebe um argumento (chamado v), e retorna o retorno invertido de chamar isString com v.

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);
TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotString = (
  (fn) => (v) =>
    !fn(v)
)(isString);

O mesmo funciona para isNotNumber. Chamando makeIsNot com isNumber, retorna uma função que recebe um argumento (chamado v), e retorna o retorno invertido de chamar isNumber com v.

TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotNumber = makeIsNot(isNumber);
TypeScript
const makeIsNot = (fn) => (v) => !fn(v);

const isNotNumber = (
  (fn) => (v) =>
    !fn(v)
)(isNumber);

LinkAssinatura makeIsNot

Tudo muito bem e muito bom com a implementação, agora, para a assinatura de tipo de makeIsNot.

TypeScript
type MakeIsNot = <F extends (v: unknown) => v is any>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, F extends (v: unknown) => v is infer T ? T : never>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

Vamos decompor isso e ver o que podemos simplificar.

LinkType Guard Function Type

Primeiramente, existem dois lugares onde nós estamos nos referindo a uma função que retorna um type predicate (em outras palavras, uma type guard personalizada).

Vamos criar um tipo, chamado TypeGuardFunction, para isolar essa definição de tipo e simplificar um pouco o nosso código.

TypeScript
type TypeGuardFunction<T = any> = (v: unknown) => v is T;

type MakeIsNot = <F extends TypeGuardFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, F extends TypeGuardFunction<infer T> ? T : never>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

Um pouco melhor, né?

LinkPredicate Function Type

Além disso, embora o nome TypeGuardFunction faça muito sentido, já que é de fato uma type guard function, esse nome é muito específico do TypeScript. E as funções que recebem um argumento e retornam um boolean já tinham um nome antes mesmo do TypeScript existir. Essas funções são conhecidas como "Predicate Functions".

Então, vamos usar o nome PredicateFunction.

TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, F extends PredicateFunction<infer T> ? T : never>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

LinkUnpack Predicate Function Type

Além disso, não que se repita, mas essa parte onde nós estamos inferindo o tipo de PredicateFunction é um pouco feia de se olhar. Vamos isolar isso em um tipo.

Falando nisso, se você está perdido com o operador ternário e a keyword infer, tenho dois vídeos para você. Os dois tem só um minuto de duração. Um explica os tipos condicionais no TypeScript (o operador ternário), e o outro explica a keyword infer. Vou deixar o link deles na descrição.

Eu tenho outra convenção pessoal que é usar o prefixo Unpack quando estou criando um tipo que infere algo. Então vou chamar UnpackPredicateFunction.

TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, UnpackPredicateFunction<F>>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

LinkDe volta para a assinatura

Ok... não é tão simples. Mas está mais simples do que antes. Vamos tentar analisar a assinatura agora.

  1. Primeiramente, nós recebemos um argumento, uma PredicateFunction chamada fn;
  2. Então, nós retornamos uma nova função;
  3. Essa nova função recebe um argumento, chamado v. Que tem o mesmo tipo do primeiro parâmetro de fn;
  4. E isso, retorna um type predicate dizendo que v não é do tipo protegido pela nossa função fn.
TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, UnpackPredicateFunction<F>>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

LinkBiblioteca

Existem algumas limitações para a nossa função. Por exemplo, agora, ela só funciona com guards que recebem um único argumento. Além disso, ela não foi testada.

TypeScript
type PredicateFunction<T = any> = (v: unknown) => v is T;

type UnpackPredicateFunction<F extends PredicateFunction> =
  F extends PredicateFunction<infer T> ? T : never;

type MakeIsNot = <F extends PredicateFunction>(
  fn: F
) => <V extends Parameters<F>[0] = Parameters<F>[0]>(
  v: V
) => v is Exclude<V, UnpackPredicateFunction<F>>;

const makeIsNot: MakeIsNot = (fn) => (v) => !fn(v);

const isNotString = makeIsNot(isString);

Se você está interessado em ter makeIsNot na sua base de código (e também makeIsInstance, makeIsIncluded, e muito mais), em vez de copiar o código desse vídeo, uma forma melhor é simplesmente instalar a minha biblioteca de utilidades de TypeScript.

TypeScript
import { makeIsNot } from '@lucaspaganini/ts';

const isNotString = makeIsNot(isString);
TypeScript
import { makeIsNot, makeIsInstance } from '@lucaspaganini/ts';

const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
TypeScript
import { makeIsNot, makeIsInstance, makeIsIncluded } from '@lucaspaganini/ts';

const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
const isFamousCat = makeIsIncluded(['Garfield', 'Tom']);
  • É open source
  • Tem testes
  • Documentação
  • Funciona em Node e navegadores
  • É MIT
  • E você pode facilmente instalar com npm install @lucaspaganini/ts.
bash
npm install @lucaspaganini/ts

Nós vamos falar mais sobre essa biblioteca no último artigo.

LinkConclusão

O conteúdo de hoje foi bem avançado. Eu me lembro do quão difícil foi aprender programação funcional e notações avançadas de TypeScript, então, realmente fizemos o nosso melhor com os exemplos e animações para, idealmente, oferecer a você uma experiência de aprendizado mais fácil do que a que eu tive.

Eu adoraria receber seu feedback. Então por favor, me mande um tweet e me conte se você conseguiu entender tudo, e suas dúvidas, se você tiver alguma.

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

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

No próximo vídeo, usaremos o nosso conhecimento recém-descoberto para criar uma solução alternativa para um recurso altamente solicitado em TypeScript: type guards assíncronos. Inscreva-se para não perder.

Até lá, tenha um ótimo dia, e te vejo na próxima.

LinkReferências

  1. Higher Order Functions Documentação do Clojure
  2. Programação Funcional - Predicate Functions Stanford Education
  3. Biblioteca de Utilitários TypeScript - @lucaspaganini/ts Repositório 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