Menu

Mapped types em TypeScript: sintaxe, modificadores e remapeamento

Um mapped type cria um novo tipo de objeto percorrendo chaves: { [K in keyof T]: ... }. Veja a sintaxe, os modificadores readonly e ? com + e -, o remapeamento de chaves com as, a filtragem de chaves e como Partial, Readonly, Required, Pick e Record são escritos.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um mapped type cria um novo tipo de objeto percorrendo um conjunto de chaves. { [K in keyof T]: boolean } significa "para cada chave K de T, uma propriedade chamada K do tipo boolean":

Adicione uma chave a Features e Flags<Features> passa a exigi-la também. Mapped types existem só em tempo de compilação: eles descrevem objetos, não os criam.

A sintaxe

Um mapped type tem três partes: uma variável de chave, uma union de chaves a percorrer e o tipo da propriedade, que pode usar a variável de chave.

type MappedType = {
  [K in Keys]: PropertyType; // K takes each member of Keys in turn
};

Keys pode ser qualquer union de strings, números ou symbols. Ela não precisa vir de keyof:

É exatamente o que Record<Size, number> faz; o Record é um mapped type de uma linha. O tipo da propriedade pode usar a chave: { [K in keyof T]: T[K] } copia cada tipo de propriedade por meio de um indexed access, e { [K in keyof T]: T[K] | null } torna toda propriedade anulável.

Modificadores: readonly e ? com + e -

Um mapped type pode acrescentar ou remover os modificadores readonly e opcional (?) de todas as propriedades de uma vez. Use o prefixo + para acrescentar (o padrão quando você não escreve nenhum) e - para remover:

A última linha mostra que o readonly é só uma regra de tempo de compilação: a atribuição foi apontada (e suprimida aqui com @ts-expect-error), mas o JavaScript gerado a executou mesmo assim. O -? faz mais do que tirar o ponto de interrogação: ele também remove undefined do tipo da propriedade, então AllRequired<Account> rejeita { id: 1, email: undefined } com error TS2322: Type 'undefined' is not assignable to type 'string'.

Como Partial, Readonly, Pick e Record são feitos

Os utility types nativos que remodelam objetos são mapped types. Estas são as definições do próprio lib.es5.d.ts do TypeScript:

type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };

Lê-las é um bom exercício: o Pick percorre só as chaves que você passa, e o Record ignora qualquer tipo de origem e dá a toda chave o mesmo tipo de valor. O Omit não é um mapped type próprio: ele é Pick<T, Exclude<keyof T, K>>. A referência de utility types lista todos.

Remapeamento de chaves com as

Uma cláusula as depois da chave muda o nome da propriedade. Combinada com template literal types, ela pode gerar nomes novos a partir dos antigos:

O string & K está ali porque keyof T pode incluir chaves number e symbol, e o Capitalize só aceita strings. A intersection mantém as chaves string e descarta o resto.

Filtrando chaves com never

Se a cláusula as produz never para uma chave, essa chave é removida. Com um conditional type você pode manter ou descartar propriedades de acordo com o tipo delas:

Tipos como DataOnly são úteis para descrever o que sobrevive ao JSON.stringify ou o que um formulário edita: os campos sem os métodos.

Quais modificadores são mantidos

Um mapped type sobre keyof T para algum tipo T é chamado de homomórfico, e copia de T os modificadores readonly e ? de cada propriedade. Um mapped type sobre uma union simples de chaves começa sem nenhum modificador:

type Account = { readonly id: number; email?: string };

type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string }  (modifiers kept)

type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined }  (email is required now)

B ainda tem undefined no tipo de email, porque Account["email"] o inclui, mas a propriedade em si não é mais opcional: { id: 1 } é rejeitado. É por isso que Partial<T> e seus parentes percorrem keyof T: eles mantêm tudo o que não mudam explicitamente.

Um mapped type homomórfico genérico aplicado a um array ou tupla produz um array ou tupla, e não um objeto com chaves numéricas. Readonly<string[]> é readonly string[], e com type Box<T> = { [K in keyof T]: { value: T[K] } }, Box<[string, number]> é [{ value: string }, { value: number }].

Mapped types e código de tempo de execução

Como um mapped type só descreve um objeto, uma função que monta esse objeto precisa do próprio loop em tempo de execução. O tipo então descreve o resultado dela:

Os callbacks recebem os tipos dos parâmetros a partir do mapped type (v é string em um e number no outro) sem nenhuma anotação. Se você chegou aqui procurando a classe Map, e não o mapeamento de tipos, veja Map em TypeScript.

Perguntas frequentes

O que é um mapped type no TypeScript?

Um tipo que cria um tipo de objeto percorrendo uma union de chaves: { [K in Keys]: SomeType }. Na maioria das vezes as chaves são keyof T, então o novo tipo tem as mesmas chaves de T com os tipos de propriedade transformados, como em type Flags<T> = { [K in keyof T]: boolean }.

O que +readonly, -readonly, +? e -? significam em um mapped type?

Eles acrescentam ou removem modificadores. readonly ou +readonly torna toda propriedade readonly, -readonly remove o readonly. ? ou +? torna toda propriedade opcional, -? as torna obrigatórias e também remove undefined dos tipos delas. Required<T> é escrito com -?.

Como renomear chaves em um mapped type?

Use uma cláusula as depois da chave: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } transforma name em getName. Mapear uma chave para never na cláusula as a remove, e é assim que você filtra propriedades.

Um mapped type é o mesmo que Map no TypeScript?

Não. Um mapped type é uma transformação de tipos em tempo de compilação e não gera código. Map<K, V> é a classe Map do JavaScript, uma coleção de pares chave e valor em tempo de execução. Buscas por "typescript map type" muitas vezes querem dizer um ou outro.

Como Partial e Readonly são implementados?

Como mapped types na biblioteca padrão: type Partial<T> = { [P in keyof T]?: T[P] } e type Readonly<T> = { readonly [P in keyof T]: T[P] }. Pick e Record também são mapped types; Omit é Pick combinado com Exclude.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR