Un mapped type construye un tipo objeto nuevo recorriendo un conjunto de claves. { [K in keyof T]: boolean } significa «para cada clave K de T, una propiedad llamada K de tipo boolean»:
Si añades una clave a Features, Flags<Features> también la exige. Los mapped types solo existen en compilación: describen objetos, no los crean.
La sintaxis
Un mapped type tiene tres partes: una variable de clave, una unión de claves que recorrer y el tipo de la propiedad, que puede usar la variable de clave.
type MappedType = {
[K in Keys]: PropertyType; // K takes each member of Keys in turn
};
Keys puede ser cualquier unión de strings, números o símbolos. No tiene por qué venir de keyof:
Es exactamente lo que hace Record<Size, number>; Record es un mapped type de una línea. El tipo de la propiedad puede usar la clave: { [K in keyof T]: T[K] } copia el tipo de cada propiedad mediante un indexed access, y { [K in keyof T]: T[K] | null } hace que todas las propiedades admitan null.
Modificadores: readonly y ? con + y -
Un mapped type puede añadir o quitar los modificadores readonly y opcional (?) de todas las propiedades a la vez. El prefijo + añade (es lo que se aplica si no escribes ninguno) y - quita:
La última línea muestra que readonly es solo una regla de compilación: la asignación se detectó (y aquí se suprimió con @ts-expect-error), pero el JavaScript generado la ejecutó igualmente. -? hace algo más que quitar el signo de interrogación: también quita undefined del tipo de la propiedad, así que AllRequired<Account> rechaza { id: 1, email: undefined } con error TS2322: Type 'undefined' is not assignable to type 'string'.
Cómo se construyen Partial, Readonly, Pick y Record
Los utility types integrados que transforman objetos son mapped types. Estas son las definiciones del propio lib.es5.d.ts de TypeScript:
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };
Leerlas es buena práctica: Pick recorre solo las claves que le pasas, y Record ignora cualquier tipo de origen y da a cada clave el mismo tipo de valor. Omit no es un mapped type propio: es Pick<T, Exclude<keyof T, K>>. La referencia de utility types los recoge todos.
Remapeo de claves con as
Una cláusula as después de la clave cambia el nombre de la propiedad. Combinada con template literal types, puede generar nombres nuevos a partir de los antiguos:
string & K está ahí porque keyof T puede incluir claves number y symbol, y Capitalize solo acepta strings. La intersección conserva las claves string y descarta el resto.
Filtrar claves con never
Si la cláusula as produce never para una clave, esa clave desaparece. Con un conditional type puedes conservar o descartar propiedades según su tipo:
Tipos como DataOnly sirven para describir lo que sobrevive a JSON.stringify o lo que edita un formulario: los campos sin los métodos.
Qué modificadores se conservan
Un mapped type sobre keyof T para algún tipo T se llama homomórfico, y copia de T los modificadores readonly y ? de cada propiedad. Un mapped type sobre una simple unión de claves empieza sin modificadores:
type Account = { readonly id: number; email?: string };
type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string } (modifiers kept)
type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined } (email is required now)
B sigue teniendo undefined en el tipo de email, porque Account["email"] lo incluye, pero la propiedad ya no es opcional: { id: 1 } se rechaza. Por eso Partial<T> y compañía recorren keyof T: conservan todo lo que no cambian de forma explícita.
Un mapped type homomórfico genérico aplicado a un array o a una tupla produce un array o una tupla, no un objeto con claves numéricas. Readonly<string[]> es readonly string[], y con type Box<T> = { [K in keyof T]: { value: T[K] } }, Box<[string, number]> es [{ value: string }, { value: number }].
Mapped types y código en ejecución
Como un mapped type solo describe un objeto, una función que construye ese objeto necesita su propio bucle en tiempo de ejecución. El tipo describe después su resultado:
Los callbacks reciben los tipos de sus parámetros del mapped type (v es string en uno y number en el otro) sin ninguna anotación. Si llegaste aquí buscando la clase Map y no la transformación de tipos, consulta Map en TypeScript.
Preguntas frecuentes
¿Qué es un mapped type en TypeScript?
Un tipo que crea un tipo objeto recorriendo una unión de claves: { [K in Keys]: SomeType }. Lo más habitual es que las claves sean keyof T, de modo que el tipo nuevo tiene las mismas claves que T con los tipos de las propiedades transformados, como en type Flags<T> = { [K in keyof T]: boolean }.
¿Qué significan +readonly, -readonly, +? y -? en un mapped type?
Añaden o quitan modificadores. readonly o +readonly hace readonly todas las propiedades, -readonly quita el readonly. ? o +? hace opcionales todas las propiedades, -? las hace obligatorias y además quita undefined de sus tipos. Required<T> está escrito con -?.
¿Cómo renombro claves en un mapped type?
Usa una cláusula as después de la clave: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } convierte name en getName. Si la cláusula as convierte una clave en never, la clave desaparece, y así se filtran propiedades.
¿Un mapped type es lo mismo que Map en TypeScript?
No. Un mapped type es una transformación de tipos en tiempo de compilación y no genera código. Map<K, V> es la clase Map de JavaScript, una colección de pares clave-valor en tiempo de ejecución. Quien busca «typescript map type» suele querer decir una cosa u otra.
¿Cómo están implementados Partial y Readonly?
Como mapped types en la librería estándar: type Partial<T> = { [P in keyof T]?: T[P] } y type Readonly<T> = { readonly [P in keyof T]: T[P] }. Pick y Record también son mapped types; Omit es Pick combinado con Exclude.