Sintaxis de mapped types
Los mapped types crean nuevos tipos iterando sobre las claves de un tipo existente. La sintaxis base utiliza la construcción [K in keyof T] para recorrer cada propiedad y transformarla:

type MiReadonly<T> = {
readonly [K in keyof T]: T[K]
}
interface Usuario {
nombre: string
email: string
edad: number
}
type UsuarioReadonly = MiReadonly<Usuario>
// { readonly nombre: string; readonly email: string; readonly edad: number }
Cada parte de la sintaxis cumple una función:
K in keyof T: itera sobre cada clave deT, asignando la clave actual aKT[K]: accede al tipo de la propiedadKenT(tipo de acceso indexado)- El resultado es un nuevo tipo con la misma estructura pero con las transformaciones aplicadas
type Opcionalizar<T> = {
[K in keyof T]?: T[K]
}
type UsuarioOpcional = Opcionalizar<Usuario>
// { nombre?: string; email?: string; edad?: number }
Los mapped types son la base de muchos utility types integrados de TypeScript como
Partial,Required,ReadonlyyRecord. Comprender su sintaxis permite crear utilidades personalizadas.
Iterar sobre uniones de strings
Los mapped types no se limitan a keyof. Pueden iterar sobre cualquier unión de strings:
type Banderas<T extends string> = {
[K in T]: boolean
}
type Permisos = Banderas<"leer" | "escribir" | "eliminar">
// { leer: boolean; escribir: boolean; eliminar: boolean }
const permisos: Permisos = {
leer: true,
escribir: true,
eliminar: false
}
type Contadores<T extends string> = {
[K in T]: number
}
type EstadisticasPagina = Contadores<"visitas" | "clics" | "conversiones">
const stats: EstadisticasPagina = {
visitas: 1500,
clics: 320,
conversiones: 45
}
Modificadores: readonly y opcionalidad
Los mapped types admiten modificadores que controlan si una propiedad es de solo lectura o es opcional. Los prefijos + y - permiten añadir o eliminar estos modificadores.
Añadir modificadores
// Hacer todas las propiedades readonly y opcionales
type ReadonlyOpcional<T> = {
+readonly [K in keyof T]+?: T[K]
}
interface Config {
host: string
puerto: number
debug: boolean
}
type ConfigInmutable = ReadonlyOpcional<Config>
// { readonly host?: string; readonly puerto?: number; readonly debug?: boolean }
El prefijo + es el comportamiento por defecto, así que +readonly equivale a escribir solo readonly, y +? equivale a ?.
Eliminar modificadores
El prefijo - elimina un modificador existente:
// Eliminar readonly de todas las propiedades
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
type UsuarioMutable = Mutable<Readonly<Usuario>>
// { nombre: string; email: string; edad: number }
// Eliminar opcionalidad (equivalente a Required)
type Obligatorio<T> = {
[K in keyof T]-?: T[K]
}
interface Formulario {
nombre?: string
email?: string
telefono?: string
}
type FormularioCompleto = Obligatorio<Formulario>
// { nombre: string; email: string; telefono: string }
Combinar eliminación de modificadores
// Eliminar tanto readonly como opcionalidad
type MutableRequerido<T> = {
-readonly [K in keyof T]-?: T[K]
}
interface DatosParciales {
readonly id?: number
readonly nombre?: string
readonly activo?: boolean
}
type DatosCompletos = MutableRequerido<DatosParciales>
// { id: number; nombre: string; activo: boolean }
El modificador
-readonlysolo tiene efecto si la propiedad original erareadonly. De la misma forma,-?solo tiene efecto sobre propiedades opcionales. Aplicarlos sobre propiedades que no tienen esos modificadores no produce error.
Key remapping con as
La cláusula as dentro de un mapped type permite renombrar las claves durante la iteración. Esta funcionalidad se combina con template literal types para crear transformaciones de nombres de propiedad:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Persona {
nombre: string
edad: number
activo: boolean
}
type PersonaGetters = Getters<Persona>
// { getNombre: () => string; getEdad: () => number; getActivo: () => boolean }
Setters con key remapping
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (valor: T[K]) => void
}
type PersonaSetters = Setters<Persona>
// { setNombre: (valor: string) => void; setEdad: (valor: number) => void; setActivo: (valor: boolean) => void }
Manejadores de eventos
type Manejadores<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
anterior: T[K],
nuevo: T[K]
) => void
}
type PersonaManejadores = Manejadores<Persona>
// {
// onNombreChange: (anterior: string, nuevo: string) => void
// onEdadChange: (anterior: number, nuevo: number) => void
// onActivoChange: (anterior: boolean, nuevo: boolean) => void
// }
Prefijos y sufijos arbitrarios
type ConPrefijo<T, Prefijo extends string> = {
[K in keyof T as `${Prefijo}_${string & K}`]: T[K]
}
interface Dimensiones {
ancho: number
alto: number
}
type DimensionesMin = ConPrefijo<Dimensiones, "min">
// { min_ancho: number; min_alto: number }
type DimensionesMax = ConPrefijo<Dimensiones, "max">
// { max_ancho: number; max_alto: number }
Filtrado de claves
La cláusula as combinada con tipos condicionales permite filtrar propiedades produciendo never para las claves que se quieren excluir:
type SoloPropiedadesDeTipo<T, TipoDeseado> = {
[K in keyof T as T[K] extends TipoDeseado ? K : never]: T[K]
}
interface Empleado {
id: number
nombre: string
email: string
salario: number
activo: boolean
departamento: string
}
type PropiedadesString = SoloPropiedadesDeTipo<Empleado, string>
// { nombre: string; email: string; departamento: string }
type PropiedadesNumber = SoloPropiedadesDeTipo<Empleado, number>
// { id: number; salario: number }
Cuando un mapped type produce
nevercomo nombre de clave, esa propiedad se elimina del tipo resultante. Este mecanismo es la base del filtrado de propiedades.
Excluir propiedades por tipo
type ExcluirPropiedadesDeTipo<T, TipoExcluido> = {
[K in keyof T as T[K] extends TipoExcluido ? never : K]: T[K]
}
type EmpleadoSinBooleanos = ExcluirPropiedadesDeTipo<Empleado, boolean>
// { id: number; nombre: string; email: string; salario: number; departamento: string }
Filtrar por nombre de propiedad
type ExcluirPorPrefijo<T, Prefijo extends string> = {
[K in keyof T as K extends `${Prefijo}${string}` ? never : K]: T[K]
}
interface DatosCompletos {
nombre: string
email: string
_interno: number
_cache: boolean
visible: boolean
}
type DatosPublicos = ExcluirPorPrefijo<DatosCompletos, "_">
// { nombre: string; email: string; visible: boolean }
Utility types personalizados con mapped types
Hacer opcionales solo ciertas propiedades
type HacerOpcional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface Producto {
id: number
nombre: string
descripcion: string
precio: number
stock: number
}
type CrearProducto = HacerOpcional<Producto, "id" | "stock">
// id y stock son opcionales, el resto obligatorio
const producto: CrearProducto = {
nombre: "Monitor",
descripcion: "Monitor 27 pulgadas",
precio: 300
}
Hacer readonly solo ciertas propiedades
type ReadonlyParcial<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
type ProductoConIdFijo = ReadonlyParcial<Producto, "id">
const p: ProductoConIdFijo = { id: 1, nombre: "Teclado", descripcion: "Mecanico", precio: 75, stock: 10 }
p.nombre = "Teclado Pro" // OK
// p.id = 2 // Error: readonly
Tipo que convierte valores a Promises
type Asyncify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K]
}
interface ServicioSync {
obtener(id: number): string
listar(): string[]
contador: number
}
type ServicioAsync = Asyncify<ServicioSync>
// {
// obtener: (id: number) => Promise<string>
// listar: () => Promise<string[]>
// contador: number
// }
Tipo que extrae solo métodos
type SoloMetodos<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K]
}
type SoloPropiedades<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K]
}
class Servicio {
nombre = "api"
versión = 1
iniciar(): void {}
detener(): void {}
estado(): string { return "activo" }
}
type MetodosServicio = SoloMetodos<Servicio>
// { iniciar: () => void; detener: () => void; estado: () => string }
type PropiedadesServicio = SoloPropiedades<Servicio>
// { nombre: string; versión: number }
Tipos de acceso indexado
Los tipos de acceso indexado (T[K]) permiten obtener el tipo de una propiedad específica de un tipo. Son fundamentales dentro de los mapped types, pero también se usan de forma independiente:
interface Respuesta {
datos: { id: number; nombre: string }[]
estado: number
mensaje: string
}
type TipoDatos = Respuesta["datos"]
// { id: number; nombre: string }[]
type ElementoDatos = Respuesta["datos"][number]
// { id: number; nombre: string }
type TipoEstado = Respuesta["estado"]
// number
Se pueden acceder múltiples propiedades usando una unión de claves:
type CamposTexto = Respuesta["mensaje" | "estado"]
// string | number
Acceso indexado con keyof
type TodosLosTipos<T> = T[keyof T]
interface Config {
host: string
puerto: number
debug: boolean
}
type ValoresConfig = TodosLosTipos<Config>
// string | number | boolean
Acceso indexado en arrays y tuplas
const colores = ["rojo", "verde", "azul"] as const
type Color = typeof colores[number]
// "rojo" | "verde" | "azul"
type Tupla = [string, number, boolean]
type PrimerElemento = Tupla[0] // string
type SegundoElemento = Tupla[1] // number
type TercerElemento = Tupla[2] // boolean
El operador keyof
El operador keyof produce una unión de las claves (nombres de propiedad) de un tipo. Es el complemento natural de los tipos de acceso indexado y la base de los mapped types:
interface Vehiculo {
marca: string
modelo: string
ano: number
electrico: boolean
}
type ClavesVehiculo = keyof Vehiculo
// "marca" | "modelo" | "ano" | "electrico"
keyof se usa frecuentemente en restricciones genéricas para garantizar acceso seguro a propiedades:
function obtenerValor<T, K extends keyof T>(obj: T, clave: K): T[K] {
return obj[clave]
}
const coche: Vehiculo = {
marca: "Toyota",
modelo: "Corolla",
ano: 2023,
electrico: false
}
const marca = obtenerValor(coche, "marca") // string
const electrico = obtenerValor(coche, "electrico") // boolean
// obtenerValor(coche, "color") // Error
keyof con tipos indexados
type Diccionario = { [clave: string]: number }
type ClavesDiccionario = keyof Diccionario
// string | number (number porque obj[0] es equivalente a obj["0"])
keyof con mapped types para transformaciones completas
type Validadores<T> = {
[K in keyof T]: (valor: T[K]) => boolean
}
interface Pedido {
producto: string
cantidad: number
precio: number
}
const validarPedido: Validadores<Pedido> = {
producto: (v) => v.length > 0,
cantidad: (v) => v > 0 && Number.isInteger(v),
precio: (v) => v > 0
}
function validar<T>(datos: T, validadores: Validadores<T>): boolean {
for (const clave in validadores) {
if (!validadores[clave](datos[clave])) {
return false
}
}
return true
}
const pedido: Pedido = { producto: "Laptop", cantidad: 2, precio: 999 }
console.log(validar(pedido, validarPedido)) // true
type DiferenciasParciales<T> = {
[K in keyof T]?: {
anterior: T[K]
nuevo: T[K]
}
}
interface Estado {
página: number
filtro: string
orden: "asc" | "desc"
}
const cambios: DiferenciasParciales<Estado> = {
página: { anterior: 1, nuevo: 2 },
orden: { anterior: "asc", nuevo: "desc" }
}
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Comprender la sintaxis de mapped types con [K in keyof T]. Aplicar modificadores +/- readonly y +/- ?. Usar key remapping con as clause y filtrado de claves. Construir utility types personalizados con mapped types. Dominar tipos de acceso indexado T[K] y el operador keyof.