Mapped types en TypeScript: keyof, in, as y remapping de propiedades

Los mapped types permiten generar un tipo nuevo recorriendo las propiedades de un tipo existente y aplicando una transformación a cada una. Son la base de utility types como Partial, Required, Readonly y Record, y cuando se combinan con as para el remapping de claves se convierten en una herramienta muy expresiva para transformar la forma de un objeto a nivel de tipos.

Sintaxis básica: in keyof

La forma más sencilla de un mapped type recorre las claves de un tipo existente con in keyof y replica o modifica cada propiedad:

// Implementación manual de Partial:
type MiPartial<T> = {
  [K in keyof T]?: T[K];
};

// Implementación manual de Readonly:
type MiReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

interface Usuario {
  id: number;
  nombre: string;
  email: string;
}

type UsuarioParcial = MiPartial<Usuario>;
// { id?: number; nombre?: string; email?: string }

type UsuarioSoloLectura = MiReadonly<Usuario>;
// { readonly id: number; readonly nombre: string; readonly email: string }

Modificadores: + y - para añadir o quitar

Los prefijos + (implícito) y - permiten añadir o eliminar los modificadores readonly y ?:

// Required elimina la opcionalidad:
type MiRequired<T> = {
  [K in keyof T]-?: T[K];   // -? elimina el ?
};

// Mutable elimina el readonly:
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

type Config = {
  readonly host: string;
  readonly puerto?: number;
};

type ConfigMutable = Mutable<Config>;
// { host: string; puerto?: number }

type ConfigCompleta = MiRequired<Config>;
// { host: string; puerto: number }  (readonly se conserva aquí)

Record: mapear sobre un conjunto de claves

Record<K, V> crea un tipo objeto cuyas claves son K y todos los valores son V. Es el mapped type más directo porque no necesita un tipo de entrada con propiedades:

// Implementación manual de Record:
type MiRecord<K extends keyof any, V> = {
  [P in K]: V;
};

type Pagina = "inicio" | "sobre" | "contacto";
type Rutas = MiRecord<Pagina, string>;
// { inicio: string; sobre: string; contacto: string }

// Combinando con tipos condicionales:
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

as: remapping de claves

TypeScript 4.1 introdujo la cláusula as en los mapped types, que permite transformar el nombre de la clave —no solo su tipo—. Se usa junto con template literal types para generar claves derivadas:

// Añadir prefijo "get" a cada propiedad como getter:
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Producto {
  nombre: string;
  precio: number;
}

type ProductoGetters = Getters<Producto>;
// { getNombre: () => string; getPrecio: () => number }

// Excluir propiedades cuyo tipo sea Function:
type SinMetodos<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

Mapped types sobre uniones

El tipo K en un mapped type puede ser cualquier tipo, no solo keyof T. Cuando K es un union type, el mapped type genera una propiedad por cada miembro del union:

type Flags<T extends string> = {
  [K in T]: boolean;
};

type Permisos = Flags<"leer" | "escribir" | "borrar">;
// { leer: boolean; escribir: boolean; borrar: boolean }

// Pick implementado con mapped type:
type MiPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type UsuarioResumen = MiPick<Usuario, "id" | "nombre">;
// { id: number; nombre: string }

Mapped types de dos niveles

// Hacer Partial profundo (solo dos niveles para no volverse recursivo aquí):
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Configuracion {
  servidor: { host: string; puerto: number };
  base_datos: { host: string; nombre: string };
}

type ConfigParcial = DeepPartial<Configuracion>;
// { servidor?: { host?: string; puerto?: number }; base_datos?: {...} }

Los mapped types son la forma idiomatic de TypeScript para transformar estructuras de tipo de forma declarativa. La clave para usarlos bien es entender que [K in keyof T] es un bucle sobre las claves, que T[K] accede al tipo de cada valor y que as da plena libertad para renombrar las claves de salida.

Imagen: Pexels / Markus Spiske

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP