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:
MutableNonNullablePropertiesObjectValuesPickPropertyByTypePickByTypemakeConstraint
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 | DateTypeScripttype ObjectValues<O> = O[keyof O];
ObjectValues<{ a: string; b: number; c: Date }>;
//=> string | number | DatePickPropertyByType 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 Dates.
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 = stringTypeScripttype 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 = stringSe 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:
AssertionFunctionUnpackAssertionFunctionassertHasPropertiesfromPredicateFunction
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: stringTypeScriptconst assertIsString: AssertionFunction<string> = (v) => {
if (typeof v !== 'string') throw Error('Not a string');
};
let aaa: number | string;
assertIsString(aaa);
aaa; // <- aaa: stringE 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>;
//=> stringTypeScriptconst assertIsString: AssertionFunction<string> = (v) => {
if (typeof v !== 'string') throw Error('Not a string');
};
UnpackAssertionFunction<typeof assertIsString>;
//=> stringassertHasProperties 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:
PredicateFunctionUnpackPredicateFunctionUnguardedPredicateFunctionAsyncPredicateFunctionAsyncUnguardedPredicateFunctionmakeIsNotmakeIsInstancemakeIsIncludedmakeHasPropertiesmakeAsyncPredicateFunctionfromAssertionFunction
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>;
//=> stringTypeScripttype 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