learning-front

Extra · Type-level TypeScript (opcional)

Mapped types

Recorrer las claves de un tipo con { [K in keyof T]: ... } y construir tu propio Partial, Readonly o Pick: los utility types dejan de ser magia.

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:

typescript
// 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.

typescript
// 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).

typescript
// ? 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.

typescript
// -? 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—.

typescript
// 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.
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.