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.
function f1() {}
TypeScriptfunction f1() {}
function f1(f2: Function) {}
TypeScriptfunction 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".
const makeIsNot = fn => ✨magic✨
TypeScriptconst makeIsNot = fn => ✨magic✨
Como você vai descobrir em breve, isso vai abrir a porta para novas possibilidades de reutilizar nosso código.
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
}
TypeScriptconst 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.
Funçõ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 queremosisNotString
. - Já temos
isNumber
, e agora queremosisNotNumber
. - 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.
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
}
TypeScripttype 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:
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;
TypeScriptconst 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 isString
e o outro está usando o guarda isNumber
.
Guardas 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?
const makeIsNot = (fn) => (v) => !fn(v);
const isNotString = makeIsNot(isString);
TypeScriptconst 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.
Implementaçã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
:
const makeIsNot = (fn) => (v) => !fn(v);
const isNotString = makeIsNot(isString);
TypeScriptconst 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
.
const makeIsNot = (fn) => (v) => !fn(v);
const isNotString = makeIsNot(isString);
TypeScriptconst makeIsNot = (fn) => (v) => !fn(v);
const isNotString = makeIsNot(isString);
const makeIsNot = (fn) => (v) => !fn(v);
const isNotString = (
(fn) => (v) =>
!fn(v)
)(isString);
TypeScriptconst 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
.
const makeIsNot = (fn) => (v) => !fn(v);
const isNotNumber = makeIsNot(isNumber);
TypeScriptconst makeIsNot = (fn) => (v) => !fn(v);
const isNotNumber = makeIsNot(isNumber);
const makeIsNot = (fn) => (v) => !fn(v);
const isNotNumber = (
(fn) => (v) =>
!fn(v)
)(isNumber);
TypeScriptconst makeIsNot = (fn) => (v) => !fn(v);
const isNotNumber = (
(fn) => (v) =>
!fn(v)
)(isNumber);
Assinatura makeIsNot
Tudo muito bem e muito bom com a implementação, agora, para a assinatura de tipo de makeIsNot
.
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);
TypeScripttype 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.
Type 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.
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);
TypeScripttype 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é?
Predicate 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
.
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);
TypeScripttype 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);
Unpack 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
.
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);
TypeScripttype 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);
De volta para a assinatura
Ok... não é tão simples. Mas está mais simples do que antes. Vamos tentar analisar a assinatura agora.
- Primeiramente, nós recebemos um argumento, uma
PredicateFunction
chamadafn
; - Então, nós retornamos uma nova função;
- Essa nova função recebe um argumento, chamado
v
. Que tem o mesmo tipo do primeiro parâmetro defn
; - E isso, retorna um type predicate dizendo que
v
não é do tipo protegido pela nossa funçãofn
.
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);
TypeScripttype 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);
Biblioteca
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.
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);
TypeScripttype 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.
import { makeIsNot } from '@lucaspaganini/ts';
const isNotString = makeIsNot(isString);
TypeScriptimport { makeIsNot } from '@lucaspaganini/ts';
const isNotString = makeIsNot(isString);
import { makeIsNot, makeIsInstance } from '@lucaspaganini/ts';
const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
TypeScriptimport { makeIsNot, makeIsInstance } from '@lucaspaganini/ts';
const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
import { makeIsNot, makeIsInstance, makeIsIncluded } from '@lucaspaganini/ts';
const isNotString = makeIsNot(isString);
const isArray = makeIsInstance(Array);
const isFamousCat = makeIsIncluded(['Garfield', 'Tom']);
TypeScriptimport { 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
.
npm install @lucaspaganini/ts
bashnpm install @lucaspaganini/ts
Nós vamos falar mais sobre essa biblioteca no último artigo.
Conclusã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.