TypeScript: Se divertindo com Tipos Avançados

Aprendendo dicas e truques avançados em TypeScript

Image for post
Image for post
Indo aos tipos e além! — smartive AG

Este artigo é destinado a pessoas que conhecem os fundamentos do TypeScript, mas não o usaram em um projeto grande e/ou não exploraram seus tipos avançados.

Meu objetivo é convencer você, baseado em uma tonelada de exemplos, que o poderoso sistema de tipos avançados do TypeScript pode fornecer soluções satisfatórias para problemas complexos de engenharia da web e pode ser usado para robustas e divertidas modelagens.

Você pode consultar os trechos de código nas próximas sessões ou assistir o vídeo:

Blocos Iniciais

Estes são tipos que você pode encontrar na documentação oficial do TypeScript. Eles são os blocos de construção iniciais para coisas mais avançadas/robustas. Pense nesta seção como uma folha de dicas.

Tipos de Interseção

type Duck = Quackable & Swimmable;

Se você pensar em tipos como conjuntos, o que você obtém aqui é a interseção de dois tipos, que é um conjunto menor. Isso pode ser confuso, pois o tipo resultante real tem uma interface “maior”, isto é, a união dos membros de ambos os tipos.

Tipos de União

Obtenha membros comuns de vários tipos (o tipo resultante é a união entre os dois, ou seja, um conjunto maior):

type Flyable = Eagle | Butterfly;

Guardas de Tipos personalizados

Condicional de conversão de tipo:

const canFly = (animal: Animal): animal is Flyable =>
typeof (animal as any).fly === 'function';

if (canFly(animal)) {
animal.fly();
}

Com a palavra-chave is, você pode definir funções para determinar “manualmente” se um valor pertence a um tipo e, em seguida, o TypeScript assume esse tipo na parte verdadeira de uma instrução condicional.

Tipo de Afirmação

Uma forma abreviada de declarar que uma variável específica está realmente definida:

person.children[0]!.name;

Tipos Literais

Você pode usar literais primitivos reais (strings, booleans etc.) como tipos:

type Move = 'ROCK' | 'PAPER' | 'SCISSOR';

Se você pensar em tipos como conjuntos, o tipo 'ROCK' é o conjunto contendo apenas o valor 'ROCK'.

Expressão “never”

Uma expressão que nunca resolve:

const throws = (): never => {
throw new Error('This never returns');
}

const loops = (): never => {
while (true) {
console.log('This never returns either');
}
}

Este tipo aparecerá magicamente em muitos pontos surpreendentes nos exemplos abaixo.

Expressão “unknown”

Um tipo mais seguro do que any para variáveis ​​que você não pode confiar:

let a: string;

let x: any;
a = x; // OK
x = a; // OK

let y: unknown;
y = a; // OK
a = y; // Erro!

Tipos Indexados

Acesse os tipos de chaves ou valores do objeto:

type Duck = {
colors: string;
feathers: number;
}

type DuckProps = keyof Duck; // = 'colors' | 'feathers'
type ColorType = Duck['colors']; // = string
type DuckValues = Duck[DuckProps] // = string | number

Tipos Mapeados

Derivar um tipo de objeto de outro:

type Map1 = { [key: string]: string };

type Map2 = { [P in 'a' | 'b' | 'c']: string };
// = { a: string, b: string, c: string }

type Map3 = { [P in 'a' | 'b' | 'c']: P };
// = { a: 'a', b: 'b', c: 'c' }

Essa poderosa abstração fará mais sentido quando ver alguns exemplos.

Tipos de Condicionais

Deixe um tipo depender de outro tipo com uma condição:

type StringOrNumber<T> = T extends boolean ? string : number;

type T1 = StringOrNumber<true>; // string
type T2 = StringOrNumber<false>; // string
type T3 = StringOrNumber<Object>; // number

Aqui está uma aplicação real para tipos condicionais:

type TypeName<T> =
T extends string ? "string":
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";

type T0 = TypeName<string>; // "string"

Com a palavra-chave infer você pode introduzir uma variável de tipo, cujo tipo será inferido:

type ElementType<T> = T extends (infer U)[] ? U : never;
type T = ElementType<[]>; // = never
type T1 = ElementType<string[]>; // = string

Padrões Diversos

Programação Funcional

Isso não é exatamente relacionado aos tipos avançados, mas é uma ilustração de como se pode programar de uma maneira leve, mas segura, com o TypeScript. No mundo Java, no paradigma do Código Limpo, existem duas categorias principais de classes: Objetos e Tipos de Dados. Tipos de Dados representam os dados (pense em getters e setters), enquanto objetos representam funcionalidade (pense em métodos). Com o TypeScript, é possível simplificar ainda mais essa abordagem, usando interfaces para representar dados, e funções para manipular dados. Nenhum boilerplate volumoso, nenhum construtor, getters e setters necessários. Apenas retorne um objeto simples e o TypeScript assegurará que esteja estruturado corretamente:

interface RawPerson {
identifier: number;
first_name: string;
last_name: string;
}

interface Person {
id: string;
fullName: string;
}

const transformPerson = (raw: RawPerson): Person => {
return {
id: `${raw.identifier}`,
fullName: `${raw.first_name} ${raw.last_name}`,
}
}

Discriminando Uniões

O TypeScript entende que você verificou exaustivamente todas as variantes possíveis de um tipo em uma instrução switch e não o forçará a usar uma instrução padrão:

type Eagle = {
kind: 'eagle';
fly: () => 'fly';
};

type Duck = {
kind: 'duck';
quack: () => 'quack';
};

type Bird = {
kind: 'bird';
};

type Animal = Eagle | Duck | Bird;

const doSomething = (animal: Animal): string => {
switch (animal.kind) {
case 'eagle':
return animal.fly();
case 'duck':
return animal.quack();
case 'bird':
return "animal.quack()";
}
}

Derivando Tipos De Constantes

Frequentemente, precisamos de tipos literais como valores e tipos. Para evitar a redundância, o último é derivado do primeiro.

const MOVES = {
ROCK: { beats: 'SCISSOR' },
PAPER: { beats: 'ROCK' },
SCISSOR: { beats: 'PAPER' },
};
type Move = keyof typeof MOVES;
const move: Move = 'ROCK';

Entrada de Dados não conhecida

Entradas de dados não conhecidas é um caso comum para aplicar unknown:

const validateInt = (s: unknown): number => {
let n;
switch (typeof s) {
case 'number':
// handle
case 'string':
// handle
// other cases
default:
throw new Error('Not a number.');
}
}

Tipos Mapeados

Crie uma versão somente leitura de um tipo:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyDuck = Readonly<Duck>;
// = { readonly color: string; readonly feathers: number }

Derive uma versão parcial de um tipo:

type Partial<T> = { [P in keyof T]?: T[P] };
type PartialDuck = Partial<Duck>;
// = { color?: string; feathers?: number }

Derive uma versão de um tipo onde todos os campos são necessários:

type PartialDuck = {
color?: string;
feathers?: number;
}
type Required<T> = { [P in keyof T]-?: T[P] };
type Duck = Required<PartialDuck>;
// = { color: string; feathers: number }

Derivar uma versão nullable de um tipo:

type Nullable<T> = { [P in keyof T]: T[P] | null }
type NullableDuck = Partial<Duck>;
// = { color: string | null; feathers: number | null }

Derive um tipo com apenas campos específicos (Pick):

type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type ColorDuck = Pick<Duck, 'color'>;

Um exemplo na prática, onde selecionamos colunas específicas de uma tabela SQL, e o TypeScript deriva automaticamente o tipo parcial para nós:

async function fetchPersonById<T extends keyof Person>(
id: string,
...fields: T[]
): Promise<Pick<Reaction, T>> {
return await knex('Person')
.where({ id })
.select(fields)
.first();
}

const reaction = await fetchPersonById(id, 'name', 'age');
// = { name: string, age: number }

Derive um registro (todas as propriedades têm o mesmo tipo dado) de um tipo:

type Record<K extends string, T> = { [P in K]: T };
type Day = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday';
type EverydayParty = Record<Day, 'party'>;
/*
= {
Monday: 'party';
Tuesday: 'party';
Wednesday: 'party';
Thursday: 'party';
Friday: 'party';
Saturday: 'party';
Sunday: 'party';
}
*/

Tipos Condicionais

Filtre Tipos de União

Filtrar/Extrair um subconjunto de um tipo:

type Filter<T, U> = T extends U ? T : never;
type T1 = Filter<"a" | "b", "a">; // = "a"

// Natively available as Extract
type T2 = Extract<"a" | "b", "a"> // = "a"

Diferencie/Exclua um subconjunto de um tipo:

type Diff<T, U> = T extends U ? never : T;
type T1 = Diff<"a" | "b", "a">; // = "b"

// Natively available as Exclude
type T2 = Exclude<"a" | "b", "a"> // = "b"

Excluir null e undefined de um tipo (tornando-o assim não anulável):

type NonNullable<T> = Diff<T, null | undefined>;
type T = NonNullable<string | null | undefined>; // = string

Omitir campos específicos de um tipo:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type PartialDuck = Omit<Duck, 'feathers'>;
// = { color: string }

Aqui está um exemplo na prática como digitar um HOC corretamente em React.js:

export interface WithUserProps {
user: User;
}

export const withUser = <P extends WithUserProps>
(Component: React.ComponentType<P>) => (props: Omit<P, WithUserProps>) => (
<Component {...props} user={getUser()} />
)

const UsernameComponent = ({ user, message }: { user: User, message: string }) => (
<div>Hi {user.username}! {message}</div>
)

const Username = withUser(UsernameComponent); // ({ message }) => JSX.Element

Combinando Tipos Mapeados

Obtenha as propriedades da função de um objeto:

type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type Duck = {
color: string,
fly: () => void,
}
type T1 = FunctionProperties<Duck>; // = { fly: () => void }
type T2 = FunctionPropertyNames<Duck>; // = "fly"

Obtenha as propriedades não funcionais de um objeto:

type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T];

type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

type Duck = {
color: string,
fly: () => void,
}
type T1 = NonFunctionProperties<Duck>; // = { color: string }
type T2 = NonFunctionPropertyNames<Duck>; // = "color"

Obtenha o tipo de retorno de uma função:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

const x = (a: string): number => 1;
type R = ReturnType<typeof x>; // = number

Obtenha o tipo de instância de uma função construtora:

type InstanceType<T extends new (...args: any[]) => any>
= T extends new (...args: any[]) => infer R ? R : any;

class Duck {}

const make2 = <T extends new (...args: any[]) => any>
(constructor: T): [InstanceType<T>, InstanceType<T>] =>
[new constructor(), new constructor()]

const ducks = make2(Duck); // = [Duck, Duck]

Promises e Redis

Outro exemplo na prática. Todos os métodos da biblioteca no npm chamada redis usam funções callback. Hoje me dia gostamos de usar Promise e async/await. Podemos derivar o novo tipo do antigo baseado em como o construímos.

import { ClientOpts, createClient } from 'redis';
import { promisify } from 'util';

export const promisifyRedis = (opts: ClientOpts) => {
const redis = createClient(opts);

const promisifiedRedis = {
setex: promisify(redis.setex),
get: promisify(redis.get),
};
const wrappedRedis: typeof promisifiedRedis = {} as any;
for (const key of Object.keys(promisifiedRedis) as (keyof typeof promisifiedRedis)[]) {
wrappedRedis[key] = promisifiedRedis[key].bind(redis);
}
return wrappedRedis;
};

export type Redis = ReturnType<typeof promisifyRedis>;
/* =
type Redis = {
setex: (arg1: string, arg2: number, arg3: string) => Promise<string>;
get: (arg1: string) => Promise<...>;
}
*/

Conclusão

Espero ter mostrado a você um pouco do poder do TypeScript! Agora vamos em frente e TYPE EVERYTHING!

Referências

Créditos ⭐️

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store