TypeScript non ha una classe dizionario o hashmap separata. Un dizionario è un oggetto semplice tipizzato con una index signature, { [key: string]: number }, lo stesso tipo scritto Record<string, number>, oppure una Map<string, number>. Tutti e tre memorizzano valori per chiave; differiscono per i tipi di chiave, per come sono tipizzate le chiavi mancanti e per come vengono serializzati.
Per chiavi stringa e dati in forma JSON, un oggetto con Record<string, T> è la scelta abituale. Per chiavi non stringa, o per voci aggiunte e rimosse di continuo, usa una Map.
Index signature
Una index signature, [key: KeyType]: ValueType, dice "qualsiasi chiave di questo tipo corrisponde a un valore di questo tipo". Il nome della chiave (key, name, userId) è solo documentazione. I tipi di chiave possono essere string, number, symbol, pattern di template literal o union di questi.
Le chiavi numeriche vengono convertite in stringhe da JavaScript, quindi un oggetto { [id: number]: string } ha comunque chiavi stringa a runtime: Object.keys({ 1: "one" }) è [ '1' ]. La index signature numerica limita solo il modo in cui puoi indicizzarlo in TypeScript.
Record<K, V>
Record<string, V> è la forma breve di { [key: string]: V }. Con una union di chiavi letterali al posto di string, diventa un dizionario fisso che deve contenere ogni chiave:
Omettere staging da urls è l'errore di compilazione TS2741 (Property 'staging' is missing...), e questo rende Record con chiavi union una tabella di lookup controllata. Partial rende ogni valore number | undefined.
Map come hash map
Una Map accetta chiavi di qualsiasi tipo, mantiene l'ordine di inserimento, ha una size e tipizza onestamente le chiavi mancanti: get restituisce V | undefined.
Verificare se una chiave esiste
Esistono diversi controlli, e non significano tutti la stessa cosa:
| Controllo | Funziona su | Attenzione a |
|---|---|---|
Object.hasOwn(obj, key) | oggetti | ES2022; usa Object.prototype.hasOwnProperty.call(obj, key) con target più vecchi |
key in obj | oggetti | Vero anche per chiavi ereditate come toString e constructor |
obj[key] !== undefined | oggetti | Non distingue una chiave mancante da una memorizzata come undefined |
if (obj[key]) | oggetti | Falso anche per i valori 0, "" e false |
map.has(key) | Map | Non restringe un map.get(key) successivo |
map.get(key) !== undefined | Map | Stessa avvertenza su undefined degli oggetti |
Il problema delle chiavi ereditate è il motivo per cui le chiavi fornite dagli utenti, come "constructor" o "__proto__", rendono rischiosi gli oggetti semplici come dizionari. Una Map non ha chiavi del genere.
Il problema del tipo delle chiavi mancanti
Su una index signature o su Record<string, T>, leggere qualsiasi chiave ha tipo T, anche una chiave che non esiste. Il compilatore ti lascia chiamare metodi su un valore che a runtime è undefined:
L'opzione del compilatore noUncheckedIndexedAccess risolve il problema: con essa, colors["grass"] ha tipo string | undefined e la chiamata a toUpperCase è un errore di compilazione finché non lo controlli. Non fa parte di strict, quindi va attivata separatamente in tsconfig.json; vedi strict mode per gli altri flag che le stanno accanto. Una Map non ha questa lacuna, perché get include sempre undefined.
Aggiungere, rimuovere e iterare
delete funziona sulle proprietà di una index signature. Su una proprietà nominata obbligatoria di un tipo oggetto è l'errore di compilazione TS2790, The operand of a 'delete' operator must be optional.
Quale usare
| Esigenza | Usa |
|---|---|
| Chiavi stringa, JSON in ingresso o in uscita | Record<string, T> |
| Un insieme fisso e noto di chiavi, tutte obbligatorie | Record<"a" | "b", T> |
| Proprietà nominate più chiavi extra arbitrarie | un tipo oggetto con una index signature |
| Chiavi che sono oggetti, numeri da tenere come numeri o qualsiasi non stringa | Map<K, V> |
| Aggiunte ed eliminazioni frequenti, o una dimensione che leggi spesso | Map<K, V> |
| Chiavi che arrivano dagli utenti | Map<K, V> (nessuna chiave ereditata) |
Domande frequenti
Come si crea un dizionario in TypeScript?
Tipizza un oggetto semplice con una index signature, const ages: { [name: string]: number } = {}, o con l'equivalente Record<string, number>. Poi aggiungi le voci con ages["ada"] = 36. Per chiavi che non sono stringhe, o per una collezione con frequenti aggiunte ed eliminazioni, usa new Map<string, number>().
TypeScript ha una HashMap?
Non con quel nome. La Map integrata di JavaScript è una hash map: Map<K, V> memorizza coppie chiave-valore con ricerca veloce per chiave, mantiene l'ordine di inserimento e accetta qualsiasi tipo di chiave. Un oggetto semplice tipizzato come Record<string, V> è l'altra scelta comune per le chiavi stringa.
Come si verifica se una chiave esiste in un dizionario TypeScript?
Per un dizionario oggetto usa Object.hasOwn(dict, key) o key in dict (che vede anche le proprietà ereditate come toString), oppure leggi il valore e confrontalo con undefined. Per una Map, usa map.has(key), oppure controlla direttamente il risultato di map.get(key), perché has non restringe un get successivo.
Che differenza c'è tra una index signature e Record?
{ [key: string]: T } e Record<string, T> descrivono lo stesso tipo. Record è più corto e accetta anche una union di chiavi specifiche, Record<"a" | "b", T>, che richiede ogni chiave. Una index signature si può combinare con proprietà nominate in un unico tipo oggetto, e la sua chiave può avere un nome che la documenta, come in { [userId: string]: User }.
Perché leggere una chiave mancante di un dizionario non dà errore?
Di default, dict[key] su una index signature o su Record<string, T> ha tipo T, anche se a runtime il valore è undefined per una chiave mancante. Attiva noUncheckedIndexedAccess in tsconfig.json e il tipo diventa T | undefined, obbligandoti a un controllo. strict non include questa opzione.