TypeScript no tiene una clase aparte para diccionarios o hashmaps. Un diccionario es o bien un objeto normal tipado con una firma de índice, { [key: string]: number }, o el mismo tipo escrito Record<string, number>, o bien un Map<string, number>. Los tres guardan valores por clave; se diferencian en los tipos de clave, en cómo se tipan las claves que faltan y en cómo se serializan.
Para claves string y datos con forma de JSON, lo habitual es un objeto con Record<string, T>. Para claves que no son strings, o para entradas que se añaden y se quitan todo el tiempo, usa un Map.
Firmas de índice
Una firma de índice, [key: KeyType]: ValueType, dice «cualquier clave de este tipo corresponde a un valor de este tipo». El nombre de la clave (key, name, userId) es solo documentación. Los tipos de clave pueden ser string, number, symbol, patrones de template literal o uniones de ellos.
JavaScript convierte las claves numéricas en strings, así que un objeto { [id: number]: string } sigue teniendo claves string en tiempo de ejecución: Object.keys({ 1: "one" }) es [ '1' ]. La firma de índice numérica solo restringe cómo puedes indexarlo en TypeScript.
Record<K, V>
Record<string, V> es una forma abreviada de { [key: string]: V }. Con una unión de claves literales en lugar de string, se convierte en un diccionario fijo que debe contener todas las claves:
Omitir staging en urls es el error de compilación TS2741 (Property 'staging' is missing...), lo que convierte a Record con claves de unión en una tabla de búsqueda comprobada. Partial hace que cada valor sea number | undefined.
Map como hash map
Un Map acepta claves de cualquier tipo, mantiene el orden de inserción, tiene size y tipa con honestidad las claves que faltan: get devuelve V | undefined.
Comprobar si existe una clave
Hay varias comprobaciones, y no todas significan lo mismo:
| Comprobación | Funciona con | Cuidado con |
|---|---|---|
Object.hasOwn(obj, key) | objetos | ES2022; usa Object.prototype.hasOwnProperty.call(obj, key) en targets antiguos |
key in obj | objetos | También es true para claves heredadas como toString y constructor |
obj[key] !== undefined | objetos | No distingue una clave que falta de una guardada como undefined |
if (obj[key]) | objetos | También es false para los valores 0, "" y false |
map.has(key) | Map | No estrecha un map.get(key) posterior |
map.get(key) !== undefined | Map | La misma salvedad con undefined que en los objetos |
El problema de las claves heredadas es la razón por la que claves que vienen del usuario, como "constructor" o "__proto__", hacen arriesgado usar objetos normales como diccionarios. Un Map no tiene esas claves.
El problema del tipo de las claves que faltan
En una firma de índice o en Record<string, T>, leer cualquier clave tiene tipo T, incluso una clave que no existe. El compilador te deja llamar a métodos sobre un valor que en tiempo de ejecución es undefined:
La opción del compilador noUncheckedIndexedAccess lo arregla: con ella, colors["grass"] tiene tipo string | undefined y la llamada a toUpperCase es un error de compilación hasta que lo compruebes. No forma parte de strict, así que hay que activarla aparte en tsconfig.json; consulta el modo strict para ver las demás opciones que la acompañan. Un Map no tiene ese hueco, porque get siempre incluye undefined.
Añadir, borrar y recorrer
delete funciona con propiedades de una firma de índice. Sobre una propiedad con nombre obligatoria de un tipo objeto es el error de compilación TS2790, The operand of a 'delete' operator must be optional.
Cuál usar
| Necesidad | Usa |
|---|---|
| Claves string, JSON de entrada o salida | Record<string, T> |
| Un conjunto fijo y conocido de claves, todas obligatorias | Record<"a" | "b", T> |
| Propiedades con nombre más claves extra arbitrarias | un tipo objeto con una firma de índice |
| Claves que son objetos, números que quieres mantener como números, o cualquier no string | Map<K, V> |
| Añadir y borrar con frecuencia, o un tamaño que consultas a menudo | Map<K, V> |
| Claves que vienen de los usuarios | Map<K, V> (sin claves heredadas) |
Preguntas frecuentes
¿Cómo creo un diccionario en TypeScript?
Tipa un objeto normal con una firma de índice, const ages: { [name: string]: number } = {}, o con el equivalente Record<string, number>. Después añade entradas con ages["ada"] = 36. Para claves que no son strings, o para una colección en la que añades y borras a menudo, usa new Map<string, number>().
¿TypeScript tiene un HashMap?
No con ese nombre. El Map integrado de JavaScript es un hash map: Map<K, V> guarda pares clave valor con búsqueda rápida por clave, mantiene el orden de inserción y acepta cualquier tipo de clave. Un objeto normal tipado como Record<string, V> es la otra opción habitual para claves string.
¿Cómo compruebo si existe una clave en un diccionario de TypeScript?
En un diccionario de objeto usa Object.hasOwn(dict, key) o key in dict (que también ve propiedades heredadas como toString), o lee el valor y compáralo con undefined. En un Map, usa map.has(key), o comprueba directamente el resultado de map.get(key), ya que has no estrecha un get posterior.
¿Qué diferencia hay entre una firma de índice y Record?
{ [key: string]: T } y Record<string, T> describen el mismo tipo. Record es más corto y también admite una unión de claves concretas, Record<"a" | "b", T>, que exige todas las claves. Una firma de índice se puede combinar con propiedades con nombre en un mismo tipo objeto, y su clave puede llevar un nombre que la documente, como en { [userId: string]: User }.
¿Por qué leer una clave que no existe no da error?
Por defecto, dict[key] sobre una firma de índice o Record<string, T> tiene tipo T, aunque en tiempo de ejecución el valor sea undefined para una clave que falta. Activa noUncheckedIndexedAccess en tsconfig.json y el tipo pasa a ser T | undefined, lo que obliga a comprobarlo. strict no incluye esta opción.