O TypeScript não tem uma classe separada de dicionário ou hashmap. Um dicionário é um objeto comum tipado com uma index signature, { [key: string]: number }, o mesmo tipo escrito como Record<string, number>, ou um Map<string, number>. Os três guardam valores por chave; eles diferem nos tipos de chave, na forma como chaves ausentes são tipadas e na serialização.
Para chaves string e dados no formato de JSON, um objeto com Record<string, T> é a escolha usual. Para chaves que não são strings, ou entradas adicionadas e removidas o tempo todo, use um Map.
Index signatures
Uma index signature, [key: KeyType]: ValueType, diz "qualquer chave deste tipo mapeia para um valor deste tipo". O nome da chave (key, name, userId) serve só como documentação. Os tipos de chave podem ser string, number, symbol, padrões de template literal ou unions desses.
Chaves numéricas são convertidas em strings pelo JavaScript, então um objeto { [id: number]: string } continua tendo chaves string em tempo de execução: Object.keys({ 1: "one" }) é [ '1' ]. A index signature numérica só restringe como você pode indexar no TypeScript.
Record<K, V>
Record<string, V> é uma forma curta de { [key: string]: V }. Com uma union de chaves literais no lugar de string, ele vira um dicionário fixo que precisa conter todas as chaves:
Deixar staging fora de urls é o erro de compilação TS2741 (Property 'staging' is missing...), o que faz de Record com chaves union uma tabela de consulta verificada. Partial torna cada valor number | undefined.
Map como hash map
Um Map aceita chaves de qualquer tipo, mantém a ordem de inserção, tem size e tipa chaves ausentes com honestidade: get retorna V | undefined.
Verificando se uma chave existe
Existem várias verificações, e elas não significam todas a mesma coisa:
| Verificação | Funciona em | Cuidado com |
|---|---|---|
Object.hasOwn(obj, key) | objetos | ES2022; use Object.prototype.hasOwnProperty.call(obj, key) em targets mais antigos |
key in obj | objetos | Também é true para chaves herdadas como toString e constructor |
obj[key] !== undefined | objetos | Não distingue uma chave ausente de uma guardada como undefined |
if (obj[key]) | objetos | Também é false para os valores 0, "" e false |
map.has(key) | Map | Não estreita um map.get(key) posterior |
map.get(key) !== undefined | Map | A mesma ressalva sobre undefined dos objetos |
O problema das chaves herdadas é o motivo pelo qual chaves vindas do usuário, como "constructor" ou "__proto__", tornam objetos comuns arriscados como dicionários. Um Map não tem esse tipo de chave.
O problema do tipo de chave ausente
Em uma index signature ou em Record<string, T>, ler qualquer chave tem o tipo T, mesmo uma chave que não existe. O compilador deixa você chamar métodos em um valor que é undefined em tempo de execução:
A opção de compilador noUncheckedIndexedAccess resolve isso: com ela, colors["grass"] tem o tipo string | undefined e a chamada a toUpperCase é erro de compilação até você fazer a verificação. Ela não faz parte de strict, então precisa ser ativada separadamente no tsconfig.json; veja strict mode para as outras flags ao lado dela. Um Map não tem essa brecha, já que get sempre inclui undefined.
Adicionando, removendo e iterando
delete funciona em propriedades de index signature. Em uma propriedade nomeada obrigatória de um tipo de objeto, é o erro de compilação TS2790, The operand of a 'delete' operator must be optional.
Qual usar
| Necessidade | Use |
|---|---|
| Chaves string, entrada ou saída em JSON | Record<string, T> |
| Um conjunto fixo e conhecido de chaves, todas obrigatórias | Record<"a" | "b", T> |
| Propriedades nomeadas mais chaves extras arbitrárias | um tipo de objeto com index signature |
| Chaves que são objetos, números que você quer manter como números, ou qualquer coisa que não seja string | Map<K, V> |
| Inserções e remoções frequentes, ou um tamanho que você lê com frequência | Map<K, V> |
| Chaves que vêm dos usuários | Map<K, V> (sem chaves herdadas) |
Perguntas frequentes
Como criar um dicionário no TypeScript?
Tipe um objeto comum com uma index signature, const ages: { [name: string]: number } = {}, ou o equivalente Record<string, number>. Depois adicione entradas com ages["ada"] = 36. Para chaves que não são strings, ou para uma coleção com inserções e remoções frequentes, use new Map<string, number>().
O TypeScript tem HashMap?
Não com esse nome. O Map nativo do JavaScript é um hash map: Map<K, V> guarda pares chave e valor com busca rápida por chave, mantém a ordem de inserção e aceita chaves de qualquer tipo. Um objeto comum tipado como Record<string, V> é a outra escolha comum para chaves string.
Como verificar se uma chave existe em um dicionário no TypeScript?
Em um dicionário de objeto, use Object.hasOwn(dict, key) ou key in dict (que também enxerga propriedades herdadas como toString), ou leia o valor e compare com undefined. Em um Map, use map.has(key) ou verifique direto o resultado de map.get(key), já que has não estreita um get posterior.
Qual é a diferença entre index signature e Record?
{ [key: string]: T } e Record<string, T> descrevem o mesmo tipo. Record é mais curto e também aceita uma union de chaves específicas, Record<"a" | "b", T>, que exige todas as chaves. Uma index signature pode ser combinada com propriedades nomeadas no mesmo tipo de objeto, e a chave pode ter um nome que a documenta, como em { [userId: string]: User }.
Por que ler uma chave inexistente de um dicionário não dá erro?
Por padrão, dict[key] em uma index signature ou Record<string, T> tem o tipo T, mesmo que o valor seja undefined em tempo de execução para uma chave ausente. Ative noUncheckedIndexedAccess no tsconfig.json e o tipo passa a ser T | undefined, o que obriga uma verificação. strict não inclui essa opção.