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.