Narrowing Library
Narrowing Library
Olá! Bem-vindo ao último vídeo da nossa série sobre TypeScript Narrowing!
Hoje, eu vou te mostrar uma biblioteca de código aberto que escrevi usando as mesmas técnicas discutidas em nossos vídeos anteriores. Esta biblioteca não é, de forma alguma, rígida para o fluxo de trabalho de ninguém, pelo contrário, fiz isso com a intenção de ser valioso para qualquer pessoa que trabalhe em uma base de código TypeScript.
Sou Lucas Paganini, e neste site lançamos tutoriais de desenvolvimento web. Inscreva-se se for do seu interesse.
Instalando e Público Alvo
Antes de falarmos sobre o que está dentro, você deve saber como instalá-lo e quem deve instalá-lo.
Disponibilizei esta biblioteca no NPM, para adicioná-la ao sua base de código, simplesmente execute npm install @lucaspaganini/ts
.
Agora, em relação a quem deve instalar, vejo esta biblioteca como "Lodash para TypeScript". Ele fornece utilitários flexíveis e seguros de tipo que tornam sua base de código mais limpa. Além disso, tudo é isolado, então você pode instalar e importar apenas o que você realmente deseja usar.
Dito isso, eu realmente acredito que esta biblioteca é útil para qualquer pessoa que trabalhe em uma base de código TypeScript. Frontend, backend, o que for... Se estiver usando o TypeScript, você se beneficiará desses utilitários.
Módulos disponíveis atualmente
Portanto, sem mais delongas, vamos explorar o que está atualmente disponível na biblioteca.
👉 A propósito, digo "atualmente disponível" porque é uma coisa viva. Nós vamos adicionar mais no futuro.
Até agora, nossa biblioteca tem 3 módulos:
- Principal.
- Asserções.
- Predicados.
Modulo Principal
Vamos começar com o módulo principal.
O módulo principal contém esses 6 utilitários:
Mutable
NonNullableProperties
ObjectValues
PickPropertyByType
PickByType
makeConstraint
Mutable
é o oposto do tipo Readonly nativo. Ele converte as propriedades readonly de um tipo em propriedades mutáveis regulares.
type Mutable<T> = { -readonly [P in keyof T]: T[P] }
Mutable<ReadonlyArray<number>>
//=> Array<number>
Mutable<{ readonly a: string }>
//=> { a: string }
TypeScripttype Mutable<T> = { -readonly [P in keyof T]: T[P] }
Mutable<ReadonlyArray<number>>
//=> Array<number>
Mutable<{ readonly a: string }>
//=> { a: string }
NonNullableProperties
é semelhante, ele converte todas as propriedades de um tipo em propriedades não anuláveis.
type NonNullableProperties<T> =
{ [P in keyof Required<T>]: NonNullable<T[P]> }
NonNullableProperties<{ a: string | null }>
//=> { a: string }
NonNullableProperties<{ b?: number }>
//=> { b: number }
NonNullableProperties<{ c: Date | undefined }>
//=> { c: Date }
TypeScripttype NonNullableProperties<T> =
{ [P in keyof Required<T>]: NonNullable<T[P]> }
NonNullableProperties<{ a: string | null }>
//=> { a: string }
NonNullableProperties<{ b?: number }>
//=> { b: number }
NonNullableProperties<{ c: Date | undefined }>
//=> { c: Date }
Daí temos ObjectValues
, que retorna um tipo que é a união dos tipos de todas as propriedades em um objeto. Portanto, se o seu objeto possui três propriedades, sendo elas uma string
, um number
e uma Date
. ObjectValues
fornecerá o tipo string | number | Date
.
👉 Eu não consigo te dizer o quão útil isso é.
type ObjectValues<O> = O[keyof O]
ObjectValues<{ a: string ; b: number; c: Date }>
//=> string | number | Date
TypeScripttype ObjectValues<O> = O[keyof O]
ObjectValues<{ a: string ; b: number; c: Date }>
//=> string | number | Date
PickPropertyByType
retorna as chaves das propriedades que correspondem ao tipo esperado.
Então, semelhante ao nosso último exemplo, se tivermos um objeto com quatro propriedades, um sendo uma string
, outra um number
as duas últimas Date
. Poderíamos usar PickPropertyByType
para obter apenas as propriedades que são string
. Ou aquelas que são number
. Ou mesmo as duas que são Date
s.
type PickPropertyByType<O, T> =
ObjectValues<{ [P in keyof O]: O[P] extends T ? P : never }>
type Test = { a: string; b: number; c: Date; d: Date }
PickPropertyByType<Test, string>
//=> "a"
PickPropertyByType<Test, number>
//=> "b"
PickPropertyByType<Test, Date>
//=> "c" | "d"
TypeScripttype PickPropertyByType<O, T> =
ObjectValues<{ [P in keyof O]: O[P] extends T ? P : never }>
type Test = { a: string; b: number; c: Date; d: Date }
PickPropertyByType<Test, string>
//=> "a"
PickPropertyByType<Test, number>
//=> "b"
PickPropertyByType<Test, Date>
//=> "c" | "d"
Da mesma forma, PickByType
retorna um objeto que contém apenas as propriedades que correspondem ao tipo esperado.
type PickByType<O, T> = Pick<O, PickPropertyByType<O, T>>
type Test = { a: string; b: number; c: Date; d: Date }
PickByType<Test, string>
//=> { a: string }
PickByType<Test, number>
//=> { b: number }
PickByType<Test, Date>
//=> { c: Date; d: Date }
TypeScripttype PickByType<O, T> = Pick<O, PickPropertyByType<O, T>>
type Test = { a: string; b: number; c: Date; d: Date }
PickByType<Test, string>
//=> { a: string }
PickByType<Test, number>
//=> { b: number }
PickByType<Test, Date>
//=> { c: Date; d: Date }
E por último, mas não menos importante, makeConstraint
nos permite definir uma restrição de tipo e ainda manter os tipos literais.
const makeConstraint =
<T>() =>
<V extends T>(v: V): typeof v =>
v;
TypeScriptconst makeConstraint =
<T>() =>
<V extends T>(v: V): typeof v =>
v;
Por exemplo, digamos que temos um tipo chamado Icon
que contém um nome e um id. Ambas as propriedades devem ser strings.
Em seguida, declaramos um ReadonlyArray<Icon>
com dois ícones, um com o id "error"
e o outro com o id "success"
.
Agora, se você tentar extrair o IconID
com base no tipo de icons
, ele será uma string
. Mas isso é muito amplo. IconID
deveria ser "error" | "success"
.
type Icon = { id: string; name: string };
const icons: ReadonlyArray<Icon> = [
{ id: 'error', name: 'Error, sorry' },
{ id: 'success', name: 'Success, yaaay' }
] as const;
type IconID = typeof icons[number]['id'];
//=> IconID = string
TypeScripttype Icon = { id: string; name: string };
const icons: ReadonlyArray<Icon> = [
{ id: 'error', name: 'Error, sorry' },
{ id: 'success', name: 'Success, yaaay' }
] as const;
type IconID = typeof icons[number]['id'];
//=> IconID = string
Se removermos a projeção de icons
para ReadonlyArray<Icon>
, obteremos o que queremos, mas perderemos a segurança de tipo dos icons
.
type Icon = { id: string; name: string };
const icons = [
{ id: 'error', name: 'Error, sorry' },
{ id: 'success', name: 'Success, yaaay' }
] as const;
type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
TypeScripttype Icon = { id: string; name: string };
const icons = [
{ id: 'error', name: 'Error, sorry' },
{ id: 'success', name: 'Success, yaaay' }
] as const;
type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
type Icon = { id: string; name: string };
const icons = [
{ id: 'error', foo: 'Error, sorry' },
{ id: 'success', bar: 'Success, yaaay' }
] as const;
type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
TypeScripttype Icon = { id: string; name: string };
const icons = [
{ id: 'error', foo: 'Error, sorry' },
{ id: 'success', bar: 'Success, yaaay' }
] as const;
type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
É aí que makeConstraint
entra em jogo.
const makeConstraint =
<T>() =>
<V extends T>(v: V): typeof v =>
v
type Icon = { id: string; name: string }
const iconsConstraint = makeConstraint<ReadonlyArray<Icon>>()
const icons = iconsConstraint([
{ id: 'error', foo: 'Error, sorry' }, //=> Error
{ id: 'success', bar: 'Success, yaaay' }, => Error
] as const)
type IconID = typeof icons[number]['id']
//=> IconID = "error" | "success"
TypeScriptconst makeConstraint =
<T>() =>
<V extends T>(v: V): typeof v =>
v
type Icon = { id: string; name: string }
const iconsConstraint = makeConstraint<ReadonlyArray<Icon>>()
const icons = iconsConstraint([
{ id: 'error', foo: 'Error, sorry' }, //=> Error
{ id: 'success', bar: 'Success, yaaay' }, => Error
] as const)
type IconID = typeof icons[number]['id']
//=> IconID = "error" | "success"
Com ele, podemos ter certeza de que os icons
são um ReadonlyArray<Icon>
mas ainda obtêm seus tipos readonly literais.
const makeConstraint =
<T>() =>
<V extends T>(v: V): typeof v =>
v;
type Icon = { id: string; name: string };
const iconsConstraint = makeConstraint<ReadonlyArray<Icon>>();
const icons = iconsConstraint([
{ id: 'error', name: 'Error, sorry' },
{ id: 'success', name: 'Success, yaaay' }
] as const);
type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
TypeScriptconst makeConstraint =
<T>() =>
<V extends T>(v: V): typeof v =>
v;
type Icon = { id: string; name: string };
const iconsConstraint = makeConstraint<ReadonlyArray<Icon>>();
const icons = iconsConstraint([
{ id: 'error', name: 'Error, sorry' },
{ id: 'success', name: 'Success, yaaay' }
] as const);
type IconID = typeof icons[number]['id'];
//=> IconID = "error" | "success"
Módulo de Asserções
Legal, agora vamos entrar no módulo de asserções.
Este módulo contém estes 4 utilitários:
AssertionFunction
UnpackAssertionFunction
assertHasProperties
fromPredicateFunction
Uma AssertionFunction
é exatamente o que parece. Uma função que faz uma asserção de tipo.
const assertIsString: AssertionFunction<string> = (v) => {
if (typeof v !== 'string') throw Error('Not a string');
};
let aaa: number | string;
assertIsString(aaa);
aaa; // <- aaa: string
TypeScriptconst assertIsString: AssertionFunction<string> = (v) => {
if (typeof v !== 'string') throw Error('Not a string');
};
let aaa: number | string;
assertIsString(aaa);
aaa; // <- aaa: string
E UnpackAssertionFunction
retorna o tipo declarado por uma AssertionFunction
.
const assertIsString: AssertionFunction<string> = v => {
if (typeof v !== 'string') throw Error('Not a string')
}
UnpackAssertionFunction<typeof assertIsString>
//=> string
TypeScriptconst assertIsString: AssertionFunction<string> = v => {
if (typeof v !== 'string') throw Error('Not a string')
}
UnpackAssertionFunction<typeof assertIsString>
//=> string
assertHasProperties
afirma que o valor fornecido tem as propriedades fornecidas e lança se não tiver.
👉 Para manter as coisas seguras, as propriedades são digitadas como unknown
. Se você não sabe as diferenças entre any e unknown, tenho um video de um minuto para você, para entender as diferenças entre any
e unknown
.
let foo: unknown = someUnknownObject;
// Usage
foo.a; // <- Compilation error
assertHasProperties(['a'], foo);
foo.a; // <- foo: { a: unknown }
TypeScriptlet foo: unknown = someUnknownObject;
// Usage
foo.a; // <- Compilation error
assertHasProperties(['a'], foo);
foo.a; // <- foo: { a: unknown }
E o último utilitário no módulo de asserções é fromPredicateFunction
. Ele leva um PredicateFunction
, sobre o qual falaremos em um segundo, e retorna um AssertionFunction
.
Módulo de predicados
O último módulo da nossa biblioteca também é o maior. O módulo de predicados contém 11 utilitários:
PredicateFunction
UnpackPredicateFunction
UnguardedPredicateFunction
AsyncPredicateFunction
AsyncUnguardedPredicateFunction
makeIsNot
makeIsInstance
makeIsIncluded
makeHasProperties
makeAsyncPredicateFunction
fromAssertionFunction
O primeiro, PredicateFunction
, é uma type guard. Ele recebe um valor e retorna um predicado de tipo.
Você pode ficar tentado a chamar isso de "type guard", mas como mencionei no sexto vídeo desta série (aquele sobre guardas de ordem superior), a nomenclatura de "type guard" é muito específica para TypeScript, e essas funções foram denominadas "predicate functions" muito antes de o TypeScript sequer existir.
type PredicateFunction<T = any> = (v: unknown) => v is T;
const isString: PredicateFunction<string> = (v): v is string =>
typeof v === 'string';
let aaa: number | string;
if (isString(aaa)) {
aaa; // <- aaa: string
}
TypeScripttype PredicateFunction<T = any> = (v: unknown) => v is T;
const isString: PredicateFunction<string> = (v): v is string =>
typeof v === 'string';
let aaa: number | string;
if (isString(aaa)) {
aaa; // <- aaa: string
}
Parecido com UnpackAssertionFunction
, podemos usar UnpackPredicateFunction
para extrair o tipo protegido por um PredicateFunction
.
type PredicateFunction<T = any> = (v: unknown) => v is T
const isString: PredicateFunction<string> =
(v): v is string => typeof v === 'string'
type UnpackPredicateFunction<F extends PredicateFunction> =
F extends PredicateFunction<infer T> ? T : never
UnpackPredicateFunction<typeof isString>
//=> string
TypeScripttype PredicateFunction<T = any> = (v: unknown) => v is T
const isString: PredicateFunction<string> =
(v): v is string => typeof v === 'string'
type UnpackPredicateFunction<F extends PredicateFunction> =
F extends PredicateFunction<infer T> ? T : never
UnpackPredicateFunction<typeof isString>
//=> string
Às vezes, temos funções de predicado que não retornam um predicado de tipo, apenas retornam um boolean
regular. Nestes casos, nós temos a UnguardedPredicateFunction
.
Por exemplo, isEqual
é uma UnguardedPredicateFunction
.
type UnguardedPredicateFunction<Params extends Array<any> = Array<any>> = (
...args: Params
) => boolean;
const isEqual = (a: number, b: number): boolean => a === b;
TypeScripttype UnguardedPredicateFunction<Params extends Array<any> = Array<any>> = (
...args: Params
) => boolean;
const isEqual = (a: number, b: number): boolean => a === b;
Então temos AsyncPredicateFunction
, AsyncUnguardedPredicateFunction
e makeAsyncPredicateFunction
. Não vou me aprofundar neles porque o sétimo artigo de nossa série TypeScript Narrowing foi todo sobre eles, então não vou gastar o seu tempo repetindo informações.
type AsyncPredicateFunction<T = any> = (
value: unknown
) => Promise<PredicateFunction<T>>;
type AsyncUnguardedPredicateFunction<Params extends Array<any> = Array<any>> = (
...args: Params
) => Promise<boolean>;
type MakeAsyncPredicateFunction = {
<F extends AsyncUnguardedPredicateFunction>(fn: F): (
...args: Parameters<F>
) => Promise<UnguardedPredicateFunction<Parameters<F>>>;
<T>(fn: AsyncUnguardedPredicateFunction): AsyncPredicateFunction<T>;
};
TypeScripttype AsyncPredicateFunction<T = any> = (
value: unknown
) => Promise<PredicateFunction<T>>;
type AsyncUnguardedPredicateFunction<Params extends Array<any> = Array<any>> = (
...args: Params
) => Promise<boolean>;
type MakeAsyncPredicateFunction = {
<F extends AsyncUnguardedPredicateFunction>(fn: F): (
...args: Parameters<F>
) => Promise<UnguardedPredicateFunction<Parameters<F>>>;
<T>(fn: AsyncUnguardedPredicateFunction): AsyncPredicateFunction<T>;
};
makeIsNot
também foi mencionado anteriormente, no sexto vídeo. Ele pega uma PredicateFunction
e retorna a versão invertida dela.
const isNumber: PredicateFunction<number> = (v): v is number =>
typeof v === 'number';
const isNotNumber = makeIsNot(isNumber);
let aaa: number | string | Date;
if (isNotNumber(aaa)) {
aaa; // -> aaa: string | Date
} else {
aaa; // -> aaa: number
}
TypeScriptconst isNumber: PredicateFunction<number> = (v): v is number =>
typeof v === 'number';
const isNotNumber = makeIsNot(isNumber);
let aaa: number | string | Date;
if (isNotNumber(aaa)) {
aaa; // -> aaa: string | Date
} else {
aaa; // -> aaa: number
}
makeIsInstance
é novo. Ele pega um construtor de classe e retorna uma PredicateFunction
que verifica se o valor é uma instanceof
do construtor de classe fornecido.
const makeIsInstance =
<C extends new (...args: any) => any>(
classConstructor: C
): PredicateFunction<InstanceType<C>> =>
(v): v is InstanceType<C> =>
v instanceof classConstructor;
// The following expressions are equivalent:
const isDate = makeIsInstance(Date);
const isDate = (v: any): v is Date => v instanceof Date;
TypeScriptconst makeIsInstance =
<C extends new (...args: any) => any>(
classConstructor: C
): PredicateFunction<InstanceType<C>> =>
(v): v is InstanceType<C> =>
v instanceof classConstructor;
// The following expressions are equivalent:
const isDate = makeIsInstance(Date);
const isDate = (v: any): v is Date => v instanceof Date;
makeIsIncluded
recebe um Iterable
e retorna um PredicateFunction
que verifica se o valor está incluído no iterável fornecido.
const makeIsIncluded = <T>(iterable: Iterable<T>): PredicateFunction<T> => {
const set = new Set(iterable);
return (v: any): v is T => set.has(v);
};
// The following expressions are equivalent:
const abc = ['a', 'b', 'c'];
const isInABC = makeIsIncluded(abc);
const isInABC = (v: any): v is 'a' | 'b' | 'c' => abc.includes(v);
TypeScriptconst makeIsIncluded = <T>(iterable: Iterable<T>): PredicateFunction<T> => {
const set = new Set(iterable);
return (v: any): v is T => set.has(v);
};
// The following expressions are equivalent:
const abc = ['a', 'b', 'c'];
const isInABC = makeIsIncluded(abc);
const isInABC = (v: any): v is 'a' | 'b' | 'c' => abc.includes(v);
E finalmente, assim como no módulo de asserções, temos makeHasProperties
e fromAssertionFunction
.
makeHasProperties
recebe um Array de propriedades e retorna uma PredicateFunction
que verifica se o valor tem essas propriedades.
let foo: unknown = someUnknownObject;
// Usage
foo.a; // <- Compilation error
const hasPropA = makeHasProperties(['a']);
if (hasPropA(foo)) {
foo.a; // <- foo: { a: unknown }
}
TypeScriptlet foo: unknown = someUnknownObject;
// Usage
foo.a; // <- Compilation error
const hasPropA = makeHasProperties(['a']);
if (hasPropA(foo)) {
foo.a; // <- foo: { a: unknown }
}
E fromAssertionFunction
recebe uma AssertionFunction
e retorna um PredicateFunction
.
type Assert1 = (v: unknown) => asserts v is 1;
const assert1: Assert1 = (v: unknown): asserts v is 1 => {
if (v !== 1) throw Error('');
};
const is1 = fromAssertionFunction(assert1);
declare const aaa: 1 | 2 | 3;
if (is1(aaa)) {
// <- aaa: 1
} else {
// <- aaa: 2 | 3
}
TypeScripttype Assert1 = (v: unknown) => asserts v is 1;
const assert1: Assert1 = (v: unknown): asserts v is 1 => {
if (v !== 1) throw Error('');
};
const is1 = fromAssertionFunction(assert1);
declare const aaa: 1 | 2 | 3;
if (is1(aaa)) {
// <- aaa: 1
} else {
// <- aaa: 2 | 3
}
Series Outro
É o fim, mas não feche o artigo ainda, tenho algumas coisas para dizer.
Este é o último artigo de nossa série sobre TypeScript Narrowing. A primeira série que fiz no aqui e no YouTube.
Estou muito feliz com a qualidade que conseguimos oferecer, mas também tenho grandes sonhos. Eu quero tornar as coisas loucamente melhores! E é por isso que eu e minha equipe estamos construindo uma plataforma para experiências de aprendizagem interativas.
Então imagine consumir meu conteúdo e no meio dele há um pequeno jogo para você, ou uma animação 3D, ou um teste rápido para consolidar seus conhecimentos. Você pegou a ideia.
E tudo isso, disponível em vários idiomas. Atualmente oferecemos nosso conteúdo em inglês e português. Mas também quero oferecer em espanhol, alemão, francês e muitos outros!
Por enquanto, estamos lançando todo esse conteúdo gratuitamente, mas acho que é óbvio dizer que eventualmente teremos cursos pagos, e eu quero que eles sejam bons pra c!! Tipo, quero que entreguem um valor inacreditável!
Então, com certeza, se você ainda não fez isso, eu recomendo fortemente que você
Muito obrigado por acompanhar a série. Espero que tenha gostado e espero que este seja apenas o começo de uma longa jornada no nosso site e na criação de conteúdo em geral. 🙂
Conclusão
Como sempre, as referências estão abaixo.
E se sua empresa está procurando desenvolvedores web remotos, considere entrar em contato comigo e com minha equipe em lucaspaganini.com.
Até então, tenha um ótimo dia e nos vemos na próxima.
Conteúdo Relacionado
- TypeScript Narrowing pt. 1 - 8