Record<K, V> é um utility type embutido para um objeto cujas chaves têm tipo K e cujos valores têm todos o tipo V. Com chaves string ele descreve um dicionário; com uma union de chaves literais ele descreve um objeto que precisa ter exatamente essas chaves.
Record existe só no sistema de tipos. Em tempo de execução, os dois objetos são objetos JavaScript comuns, então funcionam com object literals, spread, JSON.stringify e tudo o mais que recebe um objeto.
Sintaxe e definição
Record<Keys, Value>
Keys precisa ser algo que possa ser chave de objeto: string, number, symbol, uma union de literais string ou number, ou um template literal type. Value pode ser qualquer tipo. A definição inteira na biblioteca padrão do TypeScript tem uma linha, um mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any é string | number | symbol, o conjunto de todos os tipos de chave possíveis. [P in K]: T cria uma propriedade do tipo T para cada membro de K. Isso explica os dois comportamentos abaixo: um K amplo como string produz uma index signature (qualquer chave), e um K em union produz uma propriedade obrigatória por membro.
Chaves em union: toda chave é obrigatória
Quando as chaves são uma union de literais, um Record precisa listar cada uma delas, e nenhuma outra. Isso transforma o compilador em uma checklist:
Adicione "cancelled" a Status e os dois objetos param de compilar até você dar ao novo status um rótulo e uma cor. Deixar uma chave de fora, ou adicionar uma que não está na union, é erro de compilação:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
O mesmo funciona com um string enum como tipo da chave: Record<Color, string> exige uma entrada por membro do enum.
Record<string, T> e a chave ausente
Com chaves string, qualquer chave é permitida, e o TypeScript tipa toda leitura como T, mesmo para uma chave que não existe. Em tempo de execução, uma chave ausente dá undefined:
Esse é o bug mais comum com Record. Há três jeitos de lidar com ele: verificar com in ou Object.hasOwn antes de ler, declarar o valor como V | undefined, ou ligar a opção do compilador noUncheckedIndexedAccess, que adiciona | undefined a toda leitura de index signature no projeto. Um Record com chaves em union não tem esse problema, porque toda chave tem existência garantida.
Partial<Record<K, V>>: só algumas chaves
Para usar uma union de chaves sem exigir todas, envolva o Record em Partial. As leituras então retornam V | undefined, o que é honesto:
Uma chave com erro de digitação, como jp, continua sendo erro, e essa é a vantagem em relação a Record<string, string>.
Percorrendo um Record
Object.keys, Object.values e Object.entries funcionam todos. O porém é o tipo da chave: Object.keys retorna string[] e Object.entries retorna [string, V][], nunca a sua union de chaves:
O TypeScript mantém as chaves como string de propósito: um objeto pode ter mais propriedades em tempo de execução do que o tipo dele lista, então prometer Plan[] seria inseguro em geral. Para um objeto que você criou a partir de um literal, como seats, o cast é seguro.
Montando um Record a partir de dados
Records são o resultado habitual de agrupar ou indexar um array. Comece com um objeto vazio do tipo Record e preencha:
Book["genre"] reaproveita a union da interface como tipo da chave, então adicionar um gênero a Book faz byGenre exigir uma nova entrada.
Record<string, unknown> e interfaces
Record<string, unknown> é um tipo comum para "algum objeto com chaves string". Ele aceita object literals e valores tipados com um type alias, mas uma interface é rejeitada:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
Interfaces podem ser estendidas por declaration merging, então o TypeScript não presume que elas se encaixam em uma index signature; type aliases não podem ser reabertos, então um type User = { name: string } passaria. As correções habituais são aceitar object (você ainda pode chamar Object.keys nele), tornar a função genérica (<T extends object>(obj: T)) ou declarar User com type (a página interface vs type cobre as outras diferenças).
Record vs index signature vs Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| Existe em tempo de execução | não, é um objeto comum | não, é um objeto comum | sim, é uma classe |
| Conjunto fixo de chaves | sim, com um K em union | não | não |
| Tipos de chave | string, number, symbol, unions de literais, padrões de template | string, number, symbol, padrões de template | qualquer coisa, inclusive objetos |
| Tipo da leitura de chave ausente | V (com chaves string) | V | V | undefined vindo de get |
| Misturar com propriedades nomeadas | via interseção & | sim, no mesmo tipo | não |
| JSON e spread | sim | sim | não, converta antes |
| Tamanho | Object.keys(r).length | Object.keys(o).length | m.size |
| Adições e remoções frequentes | funciona | funciona | feito para isso |
Escolha Record com chaves em union sempre que o conjunto de chaves for conhecido: é a única opção que verifica se todas as chaves estão presentes. Para chaves string abertas, Record<string, V> e uma index signature são intercambiáveis, e muitos projetos preferem Record pela legibilidade. Use um Map quando chaves são adicionadas e removidas em tempo de execução, quando as chaves não são strings, ou quando você precisa do tamanho e da ordem de inserção sem trabalho extra.
Perguntas frequentes
O que é Record no TypeScript?
Record<K, V> é um utility type embutido para um objeto cujas chaves são do tipo K e cujos valores são todos do tipo V. Record<string, number> é um objeto com quaisquer chaves string e valores number; Record<"en" | "de", string> é um objeto com exatamente as chaves en e de, ambas strings.
Qual a diferença entre Record e Map no TypeScript?
Record é um tipo para um objeto JavaScript comum, então funciona com object literals, JSON e spread, e desaparece em tempo de compilação. Map é uma classe de tempo de execução com get, set, has e size, mantém a ordem de inserção de todas as chaves, aceita qualquer tipo de chave (objetos também), e get retorna V | undefined. Use um Record para dados fixos ou no formato de JSON e um Map para chaves adicionadas e removidas em tempo de execução.
Qual a diferença entre Record<string, T> e { [key: string]: T }?
Para os valores, são o mesmo tipo: Record<string, T> se expande em um tipo de objeto com uma index signature de string. Há duas pequenas diferenças: uma index signature pode ter um nome e ficar ao lado de outras propriedades no mesmo tipo, e keyof Record<string, T> é string, enquanto keyof { [key: string]: T } é string | number.
Como percorrer um Record no TypeScript?
Use Object.entries(record) para pares chave e valor, Object.keys para as chaves e Object.values para os valores. As chaves voltam como string, não como K, porque um objeto pode ter chaves extras em tempo de execução. Quando o Record tem uma union de chaves conhecidas, faça um cast: (Object.keys(r) as Array<keyof typeof r>).
Como deixar só algumas chaves de um Record obrigatórias?
Com chaves em union, Record<K, V> exige todas as chaves. Envolva-o em Partial para tornar todas opcionais: Partial<Record<Lang, string>>. Para misturar, use uma interseção: Record<"en", string> & Partial<Record<"de" | "fr", string>> exige en e permite as outras.