Skip to content

O kit da biblioteca padrão

Os capítulos anteriores acumularam um débito, prometido em vários callouts: as primitivas que eles ensinaram (o keyof, o acesso indexado, os tipos condicionais com distributividade e infer, os tipos mapeados com seus modificadores e a cláusula as) são exatamente as peças com que a biblioteca padrão constrói seus utility types. Reconstruí-los agora, um a um, paga esse débito e faz o arsenal deixar de ser uma lista de nomes a decorar e virar um punhado de aplicações de coisas que você já entende. A meta de cada reconstrução não é a linha de código, é reconhecer qual primitiva está por baixo.

Três dos utility types mais comuns já foram reconstruídos no capítulo de tipos mapeados, e valem uma revisão rápida antes dos novos, porque os que faltam se apoiam neles. Partial torna todo campo opcional, Required remove o opcional, Readonly torna tudo imutável, e cada um é um tipo mapeado com um modificador:

type Parcial<T> = { [K in keyof T]?: T[K] };
type Obrigatorio<T> = { [K in keyof T]-?: T[K] };
type SomenteLeitura<T> = { readonly [K in keyof T]: T[K] };

O primeiro utility genuinamente novo é o Pick, que produz um objeto só com um subconjunto das chaves. Ele é um tipo mapeado que, em vez de percorrer keyof T inteiro, percorre apenas as chaves K que você escolheu, e a restrição K extends keyof T garante que você só possa escolher chaves existentes, o mesmo casamento de keyof com restrição do getProperty:

type Escolhe<T, K extends keyof T> = { [P in K]: T[P] };
type Pessoa = { nome: string; idade: number; email: string };
type Contato = Escolhe<Pessoa, "nome" | "email">;
// ^? { nome: string; email: string }

O complemento do Pick é o Omit: em vez de dizer quais chaves ficam, você diz quais saem. A reconstrução mais direta usa o key remapping com filtro do capítulo 5, mandando para never toda chave que casa com K:

type Omite<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type SemEmail = Omite<Pessoa, "email">;
// ^? { nome: string; idade: number }

Vale saber que a biblioteca padrão define Omit de outra forma equivalente, combinando Pick com Exclude (o próximo desta lista): Pick<T, Exclude<keyof T, K>>. As duas chegam ao mesmo lugar, e reconhecer que “omitir é escolher tudo menos” já antecipa a família seguinte.

O último da família mapeada é o Record, que constrói um objeto com um conjunto de chaves, todas com o mesmo tipo de valor. Aqui as chaves não vêm de um T existente; são dadas diretamente, então a restrição é PropertyKey (o apelido da biblioteca para string | number | symbol):

type Registro<K extends PropertyKey, V> = { [P in K]: V };
type Placar = Registro<"casa" | "fora", number>;
// ^? { casa: number; fora: number }

A segunda família nasce inteira dos tipos condicionais distributivos do capítulo 4, e é a razão pela qual valeu a pena entender a distributividade em detalhe. Exclude remove de uma união os membros que casam com outra, e Extract faz o oposto, mantém só os que casam. Ambos são um condicional de uma linha, e toda a mágica está na distribuição membro a membro:

type Exclui<T, U> = T extends U ? never : T;
type Extrai<T, U> = T extends U ? T : never;
type Restante = Exclui<"a" | "b" | "c", "b">;
// ^? "a" | "c"
type SoStrings = Extrai<string | number | boolean, string>;
// ^? string

Siga o Exclui para ver a distributividade trabalhando: "a" | "b" | "c" é uma união, T é um parâmetro nu, então o condicional se aplica a cada membro. "a" não casa com "b", sobrevive; "b" casa, vira never e some; "c" sobrevive. Resta "a" | "c". É o mesmo mecanismo do SemNulo do capítulo 4, generalizado.

NonNullable é o caso particular que remove null e undefined, e é literalmente o SemNulo que você já escreveu:

type NaoNulo<T> = T extends null | undefined ? never : T;
type Definido = NaoNulo<string | null | undefined>;
// ^? string

A terceira família vem do infer, e trata de olhar dentro de tipos de função e de Promise. ReturnType extrai o tipo de retorno de uma função, casando a assinatura contra um padrão e capturando o retorno:

type TipoDeRetorno<F extends (...args: any) => any> =
F extends (...args: any) => infer R ? R : never;
function criarUsuario() {
return { id: 1, nome: "Ana" };
}
type Usuario = TipoDeRetorno<typeof criarUsuario>;
// ^? { id: number; nome: string }

O padrão (...args: any) => infer R diz “uma função com quaisquer argumentos que devolve algum R”, e o infer R captura esse retorno. O typeof criarUsuario na chamada é o operador que pega o tipo de um valor, ponte necessária para alimentar um valor (a função) a um tipo. Seu par simétrico é Parameters, que captura a lista de argumentos em vez do retorno, e o faz com o infer sobre tupla que o capítulo 4 mostrou:

type Parametros<F extends (...args: any) => any> =
F extends (...args: infer P) => any ? P : never;
function mover(x: number, y: number, suave: boolean) {}
type ArgsDeMover = Parametros<typeof mover>;
// ^? [x: number, y: number, suave: boolean]

Por fim, Awaited, que desembrulha uma Promise até o valor de dentro, é o Desembrulha recursivo do capítulo 4 promovido a oficial:

type Aguardado<T> = T extends Promise<infer U> ? Aguardado<U> : T;
type Valor = Aguardado<Promise<Promise<string>>>;
// ^? string

A versão da biblioteca é mais robusta: ela também lida com thenables (objetos que têm um método .then sem serem Promise) e com alguns casos de borda, mas o coração é este condicional recursivo. É o mesmo tipo que sustenta a inferência do await e de .then no dia a dia, agora sem mistério.

Reunindo, cada utility type é uma primitiva dos capítulos anteriores vestida com um nome. A tabela serve de índice reverso: quando esbarrar num utility, ela aponta qual mecanismo entender para lê-lo por dentro.

UtilityComo se constróiPrimitiva (capítulo)
Partial, Required, Readonlytipo mapeado com modificador ?/-?/readonlytipos mapeados (5)
Picktipo mapeado sobre as chaves escolhidasmapeados (5) + keyof (3)
Omitmapeado com filtro as, ou Pick + Excludemapeados (5) + condicionais (4)
Recordtipo mapeado sobre chaves dadastipos mapeados (5)
Exclude, Extractcondicional distributivo sobre uniãotipos condicionais (4)
NonNullablecondicional (ou T & {})tipos condicionais (4)
ReturnType, Parameterscondicional com infercondicionais e infer (4)
Awaitedcondicional com infer e recursãocondicionais e infer (4)

Esses utilitários também se compõem, e é aí que rendem no código real. Uma função que atualiza parte de um objeto combina Partial (o patch é parcial) sem perder a checagem do resto, e a função selecionar tipada com Pick é o getProperty do capítulo 3 elevado a vários campos de uma vez:

function selecionar<T, K extends keyof T>(obj: T, chaves: K[]): Pick<T, K> {
const saida = {} as Pick<T, K>;
for (const c of chaves) saida[c] = obj[c];
return saida;
}
const pessoa = { nome: "Ana", idade: 30, email: "a@x.com" };
const contato = selecionar(pessoa, ["nome", "email"]);
// ^? Pick<{ nome: string; idade: number; email: string }, "nome" | "email">
// ou seja: { nome: string; email: string }

Com o arsenal reconstruído, você já lê a maior parte dos tipos que aparecem em bibliotecas maduras. O que resta são os dois assuntos que separam quem usa generics de quem projeta APIs com eles: como a inferência decide os tipos nos casos difíceis, e onde ficam os limites do sistema. São os dois capítulos finais.

Utility types: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, Awaited Operadores de apoio: keyof, typeof, PropertyKey, infer, T & {} Conceitos: reconstrução a partir de primitivas, composição de utility types Exemplos: Escolhe, Omite, Registro, Exclui, Extrai, selecionar