Record<K, V> es un utility type integrado para un objeto cuyas claves son de tipo K y cuyos valores son todos de tipo V. Con claves string describe un diccionario; con una unión de claves literales describe un objeto que tiene que tener exactamente esas claves.
Record solo existe en el sistema de tipos. En ejecución los dos objetos son objetos normales de JavaScript, así que funcionan con objetos literales, spread, JSON.stringify y todo lo que reciba un objeto.
Sintaxis y definición
Record<Keys, Value>
Keys tiene que ser algo que pueda ser una clave de objeto: string, number, symbol, una unión de literales string o number, o un template literal type. Value puede ser cualquier tipo. La definición completa en la librería estándar de TypeScript es una línea, un mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any es string | number | symbol, el conjunto de todos los tipos de clave posibles. [P in K]: T crea una propiedad de tipo T por cada miembro de K. Eso explica los dos comportamientos siguientes: un K amplio como string produce una index signature (cualquier clave), y un K en unión produce una propiedad obligatoria por miembro.
Claves en unión: todas son obligatorias
Cuando las claves son una unión de literales, un Record tiene que incluirlas todas, y ninguna más. Así el compilador se convierte en una lista de comprobación:
Si añades "cancelled" a Status, los dos objetos dejan de compilar hasta que le des al nuevo estado una etiqueta y un color. Omitir una clave, o añadir una que no está en la unión, es un error de compilación:
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>'.
Lo mismo funciona con un enum de strings como tipo de clave: Record<Color, string> exige una entrada por cada miembro del enum.
Record<string, T> y la clave que falta
Con claves string se permite cualquier clave, y TypeScript tipa cada lectura como T, incluso para una clave que no existe. En ejecución, una clave que falta da undefined:
Es el error más común con Record. Hay tres formas de tratarlo: comprobar con in o Object.hasOwn antes de leer, declarar el valor como V | undefined, o activar la opción del compilador noUncheckedIndexedAccess, que añade | undefined a toda lectura de index signature del proyecto. Un Record con claves en unión no tiene este problema, porque todas las claves existen seguro.
Partial<Record<K, V>>: solo algunas claves
Para usar una unión de claves sin exigirlas todas, envuelve el Record en Partial. Las lecturas devuelven entonces V | undefined, que es lo honesto:
Una clave mal escrita, como jp, sigue siendo un error, y esa es la ventaja frente a Record<string, string>.
Recorrer un Record
Object.keys, Object.values y Object.entries funcionan. El problema es el tipo de las claves: Object.keys devuelve string[] y Object.entries devuelve [string, V][], nunca tu unión de claves:
TypeScript deja las claves como string a propósito: un objeto puede tener en ejecución más propiedades de las que indica su tipo, así que prometer Plan[] no sería seguro en general. Para un objeto que creaste a partir de un literal, como seats, el cast es seguro.
Construir un Record a partir de datos
Los Records son el resultado habitual de agrupar o indexar un array. Empieza con un objeto vacío del tipo Record y rellénalo:
Book["genre"] reutiliza la unión de la interfaz como tipo de clave, así que si añades un género a Book, byGenre exige una entrada nueva.
Record<string, unknown> e interfaces
Record<string, unknown> es un tipo habitual para «algún objeto con claves string». Acepta objetos literales y valores tipados con un alias type, pero rechaza una interfaz:
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'.
Las interfaces se pueden ampliar mediante declaration merging, así que TypeScript no da por hecho que encajen en una index signature; los type aliases no se pueden reabrir, así que un type User = { name: string } pasaría. Las soluciones habituales son aceptar object en su lugar (puedes seguir llamando a Object.keys sobre él), hacer la función genérica (<T extends object>(obj: T)), o declarar User con type (la página de interface vs type trata las demás diferencias).
Record frente a index signature y Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| Existe en ejecución | no, es un objeto normal | no, es un objeto normal | sí, es una clase |
| Conjunto fijo de claves | sí, con un K en unión | no | no |
| Tipos de clave | string, number, symbol, uniones de literales, patrones de plantilla | string, number, symbol, patrones de plantilla | cualquiera, también objetos |
| Tipo al leer una clave que falta | V (con claves string) | V | V | undefined desde get |
| Mezclar con propiedades con nombre | mediante intersección & | sí, en el mismo tipo | no |
| JSON y spread | sí | sí | no, hay que convertirlo antes |
| Tamaño | Object.keys(r).length | Object.keys(o).length | m.size |
| Añadir y borrar a menudo | funciona | funciona | pensado para ello |
Elige Record con claves en unión siempre que conozcas el conjunto de claves: es la única opción que comprueba que están todas. Para claves string abiertas, Record<string, V> y una index signature son intercambiables, y muchos proyectos prefieren Record por legibilidad. Recurre a un Map cuando las claves se añaden y se quitan en ejecución, cuando las claves no son strings, o cuando necesitas el tamaño y el orden de inserción sin trabajo extra.
Preguntas frecuentes
¿Qué es Record en TypeScript?
Record<K, V> es un utility type integrado para un objeto cuyas claves son de tipo K y cuyos valores son todos de tipo V. Record<string, number> es un objeto con cualquier clave string y valores number; Record<"en" | "de", string> es un objeto con exactamente las claves en y de, ambas string.
¿Qué diferencia hay entre Record y Map en TypeScript?
Record es un tipo para un objeto normal de JavaScript, así que funciona con objetos literales, JSON y spread, y desaparece al compilar. Map es una clase en tiempo de ejecución con get, set, has y size, conserva el orden de inserción de todas las claves, acepta cualquier tipo de clave (también objetos), y get devuelve V | undefined. Usa un Record para datos fijos o con forma de JSON y un Map para claves que se añaden y se quitan en ejecución.
¿Qué diferencia hay entre Record<string, T> y { [key: string]: T }?
Para los valores son el mismo tipo: Record<string, T> se expande a un tipo objeto con una index signature de tipo string. Hay dos pequeñas diferencias: una index signature puede llevar un nombre y convivir con otras propiedades en el mismo tipo, y keyof Record<string, T> es string mientras que keyof { [key: string]: T } es string | number.
¿Cómo recorro un Record en TypeScript?
Usa Object.entries(record) para los pares clave-valor, Object.keys para las claves y Object.values para los valores. Las claves llegan como string, no como K, porque un objeto puede tener claves de más en ejecución. Cuando el Record tiene una unión de claves conocidas, haz un cast: (Object.keys(r) as Array<keyof typeof r>).
¿Cómo hago obligatorias solo algunas claves de un Record?
Con claves en unión, Record<K, V> exige todas las claves. Envuélvelo en Partial para que todas sean opcionales: Partial<Record<Lang, string>>. Para una mezcla, usa una intersección: Record<"en", string> & Partial<Record<"de" | "fr", string>> exige en y permite las demás.