En el Nivel 5 usaste Partial<T>, Readonly<T> y Pick<T, K> como cajas negras: sabías lo que hacían, pero no cómo. En este capítulo vas a ver la maquinaria por dentro. Esas tres funciones de tipos —y la mayoría de los utility types de TypeScript— son mapped types: tipos que recorren las claves de otro tipo y construyen uno nuevo.
La forma base: { [K in keyof T]: T[K] }#
La sintaxis de un mapped type es análoga al for...of de JavaScript, pero a nivel de tipos. Vamos pieza a pieza:
// Forma identidad: copia T sin cambiar nada.
// K -> la clave en cada iteración ("nombre", "rol", "partidas"...)
// in keyof T -> el conjunto sobre el que se itera: todas las claves de T
// T[K] -> el tipo de esa clave (acceso indexado, lo viste en el Nivel 5)
type Identidad<T> = { [K in keyof T]: T[K] };keyof T ya lo conoces: da la unión de todas las claves de T. K in keyof T itera sobre esa unión, igual que for (const k of Object.keys(obj)) itera sobre las claves de un objeto en tiempo de ejecución. Y T[K] es el acceso indexado que recoge el tipo de esa clave concreta.
El resultado de Identidad<Heroe> es un tipo con exactamente las mismas propiedades y tipos que Heroe: no cambia nada todavía.
Transformar el valor#
Una vez que iteras sobre las claves, puedes transformar el tipo del valor que construyes. El T[K] en la parte derecha es solo un tipo más: puedes combinarlo, ampliarlo o sustituirlo.
// Añade | null al tipo de cada propiedad: ningún campo puede ser null en Heroe,
// pero aquí cada uno acepta también null.
type NulableDe<T> = { [K in keyof T]: T[K] | null };T[K] | null forma una unión entre el tipo original de la propiedad y null. Cada propiedad mantiene su nombre, pero su tipo se amplía. El mapped type no toca los nombres: solo lo que va después de los dos puntos.
Modificadores: añadir ? y readonly#
Además de transformar el tipo del valor, puedes añadir modificadores a cada propiedad. TypeScript tiene dos: ? (propiedad opcional) y readonly (de solo lectura).
// ? hace cada propiedad opcional: así está hecho Partial<T> por dentro.
type MiPartial<T> = { [K in keyof T]?: T[K] };
// readonly hace cada propiedad de solo lectura: así está hecho Readonly<T>.
type MiReadonly<T> = { readonly [K in keyof T]: T[K] };MiPartial<Heroe> y Partial<Heroe> son el mismo tipo. Ya no es una función misteriosa del compilador: es un mapped type con ?. Lo mismo con MiReadonly y Readonly.
Quitar modificadores con -? y -readonly#
Si ? añade el modificador opcional, -? lo quita. Si readonly añade el de solo lectura, -readonly lo elimina. Ese prefijo - es la forma de deshacer una transformación previa.
// -? quita el opcional: si T tenía propiedades con ?, el resultado las hace
// obligatorias. Es exactamente la definición interna de Required<T>.
type MiRequired<T> = { [K in keyof T]-?: T[K] };Esto responde a la pregunta que surgía en el Nivel 5 al ver Required<T>: ¿cómo puede TypeScript “quitar” el opcional si ya está en el tipo? Puede porque el sistema de tipos tiene ese operador -?. Aplicado en un mapped type, recorre todas las claves y, si alguna era opcional, la vuelve obligatoria.
De forma simétrica, { -readonly [K in keyof T]: T[K] } daría un tipo donde ninguna propiedad es de solo lectura, aunque T las tuviera.
Acotar las claves para reconstruir Pick#
Hasta aquí iterábamos siempre sobre keyof T: todas las claves de T. Pero el conjunto sobre el que itera el mapped type no tiene que ser keyof T: puede ser cualquier unión de claves —siempre que estén dentro de las de T—.
// En vez de "K in keyof T" (todas las claves), usamos "P in K"
// donde K es solo el subconjunto de claves que nos interesa.
// K extends keyof T garantiza que las claves de K existen en T,
// lo que hace que T[P] sea un acceso válido.
type MiPick<T, K extends keyof T> = { [P in K]: T[P] };¿Por qué K extends keyof T y no simplemente K? Porque en el cuerpo se escribe T[P]: un acceso indexado que solo es válido si P es una clave de T. Sin esa restricción, el compilador no puede garantizarlo y marca error.
MiPick<Heroe, "nombre" | "rol"> da un tipo con solo esas dos propiedades: el resultado es idéntico a Pick<Heroe, "nombre" | "rol">.
Ahora el círculo está cerrado: Partial, Readonly, Required y Pick no son privilegio del compilador. Son mapped types con el modificador o la acotación de claves correcta. En un proyecto real sigues usando los oficiales (ya existen y no hay razón para duplicarlos), pero ahora entiendes qué hacen por dentro —y eso es lo que separa usar TypeScript de comprenderlo.
Los mapped types tienen un paso más: key remapping con
as, que permite renombrar las claves mientras se itera. Lo verás en el próximo capítulo junto con los template literal types.
Comprueba lo que sabes#
Pregunta 1 de 4
¿Qué hace `{ [K in keyof T]: T[K] }`?
Tu turno#
Completa los cuatro mapped types del Team Builder hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas con Partial y Readonly, sigues con Required y cierras con Pick. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Reconstruye los utility types del Team Builder
Trabajas a nivel de tipos: completa los mapped types hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas con Partial y Readonly, sigues con Required y cierras con Pick. Al terminar, esos utility types habrán pasado de caja negra a algo que entiendes por dentro.
Paso 1: Que funcione
- MiPartial<T> convierte cada propiedad en opcional con { [K in keyof T]?: T[K] }.
- MiReadonly<T> las hace de solo lectura con { readonly [K in keyof T]: T[K] }.
- Los tests _T1 y _T2 quedan en verde.
Paso 2: Que quite modificadores
- MiRequired<T> usa -? para quitar el opcional de cada propiedad.
- Reconoces que -? es el opuesto de ?: donde ? añade el modificador, -? lo elimina.
- El test _T3 queda en verde.
Paso 3: Que cierre el círculo
- MiPick<T, K extends keyof T> acota las claves a iterar con { [P in K]: T[P] }.
- Entiendes por qué K extends keyof T es necesario: garantiza que T[P] es un acceso válido.
- Reconoces que MiPartial, MiReadonly, MiRequired y MiPick son los utility types oficiales de TypeScript reconstruidos a mano.
- Los cuatro tests en verde.
Ver soluciones
// Helper de testing de tipos (lo viste en conditional types).
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// MiPartial<T>: recorre cada clave K de T y la hace opcional con ?.
// { [K in keyof T]?: T[K] } es exactamente la definición interna de Partial<T>.
type MiPartial<T> = { [K in keyof T]?: T[K] };
// MiReadonly<T>: recorre cada clave K de T y le añade readonly.
// { readonly [K in keyof T]: T[K] } es exactamente la definición interna de Readonly<T>.
type MiReadonly<T> = { readonly [K in keyof T]: T[K] };
// MiRequired aún no está implementado en este nivel.
type MiRequired<T> = unknown;
// MiPick aún no está implementado en este nivel.
type MiPick<T, K extends keyof T> = unknown;
// Partial y Readonly ya no son magia: un mapped type con el modificador correcto.
// El nivel siguiente quita modificadores; el excelente acota las claves.
// Tests en verde para este nivel:
type _T1 = Expect<Equal<MiPartial<Heroe>, Partial<Heroe>>>;
type _T2 = Expect<Equal<MiReadonly<Heroe>, Readonly<Heroe>>>;
console.log("MiPartial y MiReadonly correctos: ya entiendes Partial y Readonly por dentro."); Por qué este nivel
- MiPartial y MiReadonly son la forma más directa de un mapped type: iterar sobre keyof T y añadir un modificador. En este nivel el trabajo del mapped type es solo el modificador; el tipo del valor (T[K]) no cambia.
- El resultado es idéntico a los utility types oficiales: ya no son una función misteriosa del compilador, son una sintaxis que puedes escribir tú.
- El nivel siguiente introduce -?, que deshace lo que ? añade.
// Helper de testing de tipos (lo viste en conditional types).
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// MiPartial<T>: añade ? a cada propiedad.
type MiPartial<T> = { [K in keyof T]?: T[K] };
// MiReadonly<T>: añade readonly a cada propiedad.
type MiReadonly<T> = { readonly [K in keyof T]: T[K] };
// MiRequired<T>: QUITA el modificador opcional con -?.
// El -? es el opuesto del ?: donde ? añade "puede faltar", -? dice "ya no puede faltar".
// { [K in keyof T]-?: T[K] } es exactamente la definición interna de Required<T>.
type MiRequired<T> = { [K in keyof T]-?: T[K] };
// MiPick aún no está implementado en este nivel.
type MiPick<T, K extends keyof T> = unknown;
// Tests en verde para este nivel:
type _T1 = Expect<Equal<MiPartial<Heroe>, Partial<Heroe>>>;
type _T2 = Expect<Equal<MiReadonly<Heroe>, Readonly<Heroe>>>;
type _T3 = Expect<Equal<MiRequired<Partial<Heroe>>, Required<Partial<Heroe>>>>;
console.log("MiRequired correcto: ya entiendes cómo -? deshace el opcional."); Por qué es mejor que el anterior
- MiRequired usa -? para eliminar el modificador opcional. Es el mismo patrón que MiPartial, con el signo cambiado: + añade, - quita.
- De forma simétrica, -readonly quitaría el readonly si se necesitara. Ambos prefijos sirven para revertir transformaciones previas.
- El salto a excelente es acotar las claves a iterar en vez de recorrer siempre todas.
// Helper de testing de tipos (lo viste en conditional types).
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Heroe {
nombre: string;
rol: string;
partidas: number;
victorias: number;
}
// MiPartial<T>: añade ? a cada propiedad.
type MiPartial<T> = { [K in keyof T]?: T[K] };
// MiReadonly<T>: añade readonly a cada propiedad.
type MiReadonly<T> = { readonly [K in keyof T]: T[K] };
// MiRequired<T>: quita el modificador opcional con -?.
type MiRequired<T> = { [K in keyof T]-?: T[K] };
// MiPick<T, K>: construye un tipo que solo tiene las claves de K.
// K extends keyof T garantiza que solo pidas claves que existen en T.
// { [P in K]: T[P] } recorre esas claves y toma el tipo de cada una desde T.
// Es exactamente la definición interna de Pick<T, K>.
type MiPick<T, K extends keyof T> = { [P in K]: T[P] };
// El cierre del círculo: MiPartial, MiReadonly, MiRequired y MiPick son,
// literalmente, Partial, Readonly, Required y Pick de la librería estándar.
// En un proyecto real usarías los de TypeScript (ya existen), pero ahora sabes
// que por dentro no son magia: un mapped type con el modificador o la acotación
// de claves adecuada. Eso es lo que separa usar TypeScript de comprenderlo.
// Tests en verde:
type _T1 = Expect<Equal<MiPartial<Heroe>, Partial<Heroe>>>;
type _T2 = Expect<Equal<MiReadonly<Heroe>, Readonly<Heroe>>>;
type _T3 = Expect<Equal<MiRequired<Partial<Heroe>>, Required<Partial<Heroe>>>>;
type _T4 = Expect<Equal<MiPick<Heroe, "nombre" | "rol">, Pick<Heroe, "nombre" | "rol">>>;
console.log("Los cuatro tests en verde: mapped types dominados."); Por qué es mejor que el anterior
- MiPick cambia el "universo" de claves: en vez de "K in keyof T" (todas), usa "P in K" donde K es el subconjunto que pide quien llama.
- K extends keyof T no es un capricho: sin esa restricción, TypeScript no puede garantizar que T[P] sea un acceso válido y marca error.
- La lección de fondo: Partial, Readonly, Required y Pick no son magia del compilador. Son mapped types con el modificador o la acotación de claves correcta. En un proyecto real usarías los oficiales, pero ahora entiendes qué hacen por dentro.