Menu

Diccionarios en TypeScript: index signatures, Record y Map

TypeScript no tiene una clase dictionary ni hashmap; una búsqueda clave valor se tipa con una firma de índice, Record<K, V> o Map<K, V>. Aprende cada una, cómo comprobar si existe una clave, añadir, borrar y recorrer, y por qué noUncheckedIndexedAccess importa en los diccionarios.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

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ónFunciona conCuidado con
Object.hasOwn(obj, key)objetosES2022; usa Object.prototype.hasOwnProperty.call(obj, key) en targets antiguos
key in objobjetosTambién es true para claves heredadas como toString y constructor
obj[key] !== undefinedobjetosNo distingue una clave que falta de una guardada como undefined
if (obj[key])objetosTambién es false para los valores 0, "" y false
map.has(key)MapNo estrecha un map.get(key) posterior
map.get(key) !== undefinedMapLa 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

NecesidadUsa
Claves string, JSON de entrada o salidaRecord<string, T>
Un conjunto fijo y conocido de claves, todas obligatoriasRecord<"a" | "b", T>
Propiedades con nombre más claves extra arbitrariasun tipo objeto con una firma de índice
Claves que son objetos, números que quieres mantener como números, o cualquier no stringMap<K, V>
Añadir y borrar con frecuencia, o un tamaño que consultas a menudoMap<K, V>
Claves que vienen de los usuariosMap<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.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR