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.

LinkInstalando 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.

LinkMó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:

  1. Principal.
  2. Asserções.
  3. Predicados.

LinkModulo Principal

Vamos começar com o módulo principal.

O módulo principal contém esses 6 utilitários:

  1. Mutable
  2. NonNullableProperties
  3. ObjectValues
  4. PickPropertyByType
  5. PickByType
  6. makeConstraint

Mutable é o oposto do tipo Readonly nativo. Ele converte as propriedades readonly de um tipo em propriedades mutáveis regulares.

TypeScript
type 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.

TypeScript
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 }

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 é.

TypeScript
type 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 Dates.

TypeScript
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"

Da mesma forma, PickByType retorna um objeto que contém apenas as propriedades que correspondem ao tipo esperado.

TypeScript
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 }

E por último, mas não menos importante, makeConstraint nos permite definir uma restrição de tipo e ainda manter os tipos literais.

TypeScript
const 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".

TypeScript
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

Se removermos a projeção de icons para ReadonlyArray<Icon>, obteremos o que queremos, mas perderemos a segurança de tipo dos icons.

TypeScript
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"
TypeScript
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"

É aí que makeConstraint entra em jogo.

TypeScript
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"

Com ele, podemos ter certeza de que os icons são um ReadonlyArray<Icon> mas ainda obtêm seus tipos readonly literais.

TypeScript
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"

LinkMódulo de Asserções

Legal, agora vamos entrar no módulo de asserções.

Este módulo contém estes 4 utilitários:

  1. AssertionFunction
  2. UnpackAssertionFunction
  3. assertHasProperties
  4. fromPredicateFunction

Uma AssertionFunction é exatamente o que parece. Uma função que faz uma asserção de tipo.

TypeScript
const 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.

TypeScript
const 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.

TypeScript
let 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.

LinkMódulo de predicados

O último módulo da nossa biblioteca também é o maior. O módulo de predicados contém 11 utilitários:

  1. PredicateFunction
  2. UnpackPredicateFunction
  3. UnguardedPredicateFunction
  4. AsyncPredicateFunction
  5. AsyncUnguardedPredicateFunction
  6. makeIsNot
  7. makeIsInstance
  8. makeIsIncluded
  9. makeHasProperties
  10. makeAsyncPredicateFunction
  11. 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.

TypeScript
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
}

Parecido com UnpackAssertionFunction, podemos usar UnpackPredicateFunction para extrair o tipo protegido por um PredicateFunction.

TypeScript
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

À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.

TypeScript
type 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.

TypeScript
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>;
};

makeIsNot também foi mencionado anteriormente, no sexto vídeo. Ele pega uma PredicateFunction e retorna a versão invertida dela.

TypeScript
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
}

makeIsInstance é novo. Ele pega um construtor de classe e retorna uma PredicateFunction que verifica se o valor é uma instanceof do construtor de classe fornecido.

TypeScript
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;

makeIsIncluded recebe um Iterable e retorna um PredicateFunction que verifica se o valor está incluído no iterável fornecido.

TypeScript
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);

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.

TypeScript
let 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.

TypeScript
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
}

LinkSeries 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ê se inscreva na newsletter. Seu suporte é altamente apreciado.

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. 🙂

LinkConclusã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.

LinkConteúdo Relacionado

  1. TypeScript Narrowing pt. 1 - 8

LinkReferências

  1. Biblioteca de utilitários para o TypeScript - @lucaspaganini/tsRepositó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