Los tipos mapeados (Mapped Types) representan un mecanismo declarativo de transformación de estructuras de datos en el sistema de tipos de TypeScript. En el núcleo de los Mapped Types reside la iteración sobre un conjunto discreto y finito de literales de clave (de tipo string, number o symbol) mediante K in keyof T, generando un nuevo contrato de objeto.
Desde el punto de vista arquitectónico del compilador, los Mapped Types se dividen en dos categorías fundamentales:
[K in keyof T]. El compilador analiza a fondo el tipo fuente T y hereda automáticamente sus metadatos internos: modificadores de inmutabilidad (readonly) y de opcionalidad (?).[K in UnionKeys] o el estándar Record<K, V>. En ellos se rompe el vínculo estructural con el objeto original, y las propiedades resultantes se generan en su estado por defecto (mutables y obligatorias).A partir de TypeScript 4.1, la potencia expresiva de los Mapped Types creció cualitativamente gracias al mecanismo Key Remapping vía as (Remapeo de claves vía as).
Sintaxis:
type Mapped<T> = {
[K in keyof T as NewKeyType<K>]: TransformedValue<T[K]>;
};La regla cardinal de Key Remapping establece: si la expresión NewKeyType<K> evalúa al tipo never, esa propiedad queda totalmente excluida de la interfaz resultante. Esto habilita un filtrado limpio de propiedades a nivel de tipos sin construcciones intermedias sintéticas.
La canalización de remapeo de claves transforma de manera desacoplada los índices (claves) y los valores de las propiedades:
+-------------------------------------------------------------------------+
| MECÁNICA DE KEY REMAPPING MEDIANTE LA CLÁUSULA "as" |
| [K in keyof T as FilterOrRename<K>]: T[K] |
+-------------------------------------------------------------------------+
| |
| Objeto origen T: { id: string; secretToken: string; count: number } |
| |
| Paso 1: Iteración sobre keyof T ("id" | "secretToken" | "count") |
| |
| Paso 2: Evaluación de la cláusula 'as': |
| - "id" as `get${Capitalize<"id">}` => "getId" |
| - "secretToken" as never (¡Propiedad eliminada!) => NEVER |
| - "count" as `get${Capitalize<"count">}` => "getCount" |
| |
| Paso 3: Emisión del contrato resultante: |
| { |
| getId: () => string; |
| getCount: () => number; |
| } |
+-------------------------------------------------------------------------+La gestión de modificadores se controla con los prefijos explícitos de adición (+) y sustracción (-):
-readonly: elimina la inmutabilidad, volviendo las propiedades mutables.
-?: elimina la opcionalidad, convirtiendo todos los campos en estrictamente obligatorios (equivalente a Required<T>).
Getters<State>).PickByValue<UserEntity, string | number>).snake_case a contratos internos en camelCase.readonly al ensamblar entidades en el patrón Builder.El operador keyof T genera la unión de literales de tipo string, number y symbol del tipo fuente.
Para cada clave K se computa la expresión de remapeo. Si el resultado es never, la clave se descarta.
Se actualizan los modificadores: adición o eliminación de flags readonly y questionToken (opcionalidad).
Para las claves conservadas se calcula el tipo de valor final y se genera el tipo anónimo resultante.
PickByValue y OmitByValue#Antes de TypeScript 4.1, filtrar propiedades requería rodeos sintácticos complejos. Con Key Remapping se expresa en una única línea declarativa:
// Extrae solo aquellas claves cuyos valores coincidan con ValueType
export type PickByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
// Excluye las claves cuyos valores coincidan con ValueType
export type OmitByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
interface UserAccount {
id: string;
username: string;
age: number;
isActive: boolean;
loginCount: number;
permissions: readonly string[];
deleteAccount: () => Promise<void>;
}
// 1. Extraemos únicamente propiedades de tipo string: { id: string; username: string }
export type StringProps = PickByValue<UserAccount, string>;
// 2. Extraemos únicamente propiedades de tipo number: { age: number; loginCount: number }
export type NumericProps = PickByValue<UserAccount, number>;
// 3. Purgamos los métodos, conservando solo datos puros (DTO serializado):
export type DataOnly = OmitByValue<UserAccount, Function>;La cláusula as se integra de forma natural con manipulaciones de cadenas en literales de plantilla:
// Generación de getters: transforma "name: string" en "getName: () => string"
export type CreateGetters<T> = {
[K in keyof T as K extends string ? `get${Capitalize<K>}` : never]: () => T[K];
};
// Generación de setters: transforma "age: number" en "setAge: (val: number) => void"
export type CreateSetters<T> = {
[K in keyof T as K extends string ? `set${Capitalize<K>}` : never]: (value: T[K]) => void;
};
interface AppConfig {
apiUrl: string;
port: number;
debugMode: boolean;
}
export type AppConfigAccessors = CreateGetters<AppConfig> & CreateSetters<AppConfig>;
/*
La interfaz resultante AppConfigAccessors contiene:
{
getApiUrl: () => string;
getPort: () => number;
getDebugMode: () => boolean;
setApiUrl: (value: string) => void;
setPort: (value: number) => void;
setDebugMode: (value: boolean) => void;
}
*/DeepMutable y DeepRequired#Las utilidades Readonly<T> y Required<T> de la librería estándar son estrictamente superficiales (Shallow). Mediante Mapped Types recursivos con sustracción de modificadores (-readonly y -?), se construyen transformadores profundos:
// Eliminación recursiva del modificador readonly en todos los niveles
export type DeepMutable<T> = {
-readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepMutable<T[K]>
: T[K];
};
// Eliminación recursiva del modificador de opcionalidad (?) en todos los niveles
export type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepRequired<T[K]>
: T[K];
};
interface ImmutableState {
readonly user: {
readonly profile: {
readonly bio?: string;
readonly socialLinks?: readonly string[];
};
};
}
// Convertimos la estructura en un borrador plenamente mutable:
export type MutableDraft = DeepMutable<ImmutableState>;
// MutableDraft.user.profile.bio ahora es escribible
// Convertimos la estructura en campos estrictamente obligatorios (sin undefined):
export type StrictState = DeepRequired<ImmutableState>;
// StrictState.user.profile.bio ahora es estrictamente 'string', no 'string | undefined'Diseñemos un adaptador de entidades para producción que sanea modelos de dominio hacia DTOs públicos, eliminando campos privados y métodos ejecutables:
// Excluimos campos privados (con prefijo _) y tokens secretos
type IsExposedKey<K> = K extends `_${string}`
? false
: K extends `${string}Password${string}`
? false
: K extends `${string}Secret${string}`
? false
: true;
// Transformador de contratos públicos para producción
export type PublicDTO<TEntity> = {
-readonly [K in keyof TEntity as IsExposedKey<K> extends true
? TEntity[K] extends Function
? never
: K
: never]: TEntity[K] extends object
? TEntity[K] extends readonly unknown[]
? TEntity[K]
: PublicDTO<TEntity[K]>
: TEntity[K];
};
// Modelo de entidad de dominio
interface UserDomainModel {
id: string;
email: string;
_internalDatabaseClusterId: number;
hashedPasswordSalt: string;
billingSecretToken: string;
profile: {
displayName: string;
_cacheVersion: number;
};
saveToDatabase: () => Promise<void>;
validatePassword: (pwd: string) => boolean;
}
export type SafeUserDTO = PublicDTO<UserDomainModel>;
/*
El compilador genera limpiamente el contrato SafeUserDTO:
{
id: string;
email: string;
profile: {
displayName: string;
};
}
¡Todos los métodos, campos privados (_) y secretos han sido purgados!
*/Un error común consiste en intentar eliminar un campo mediante T[K] extends V ? T[K] : never sin la cláusula as. Esto no suprime la propiedad; deja la clave en el objeto con el tipo never. No es posible asignar valores a never, pero la clave persiste en keyof, quebrando las iteraciones de Object.keys() y validadores en ejecución.
// ❌ INCORRECTO: conserva la clave con valor never ({ name: string, age: never })
type BadFilter<T> = {
[K in keyof T]: T[K] extends string ? T[K] : never;
};
// ✅ CORRECTO: elimina la clave mediante 'as never' ({ name: string })
type GoodFilter<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};keyof T devuelve una unión de string | number | symbol. Pasar un K no acotado a Capitalize<K> genera el error de compilación TS2344, dado que Capitalize exige una cadena. Es obligatorio acotar la clave mediante K extends string o string & K.
// ❌ ERROR TS2344: Type 'K' does not satisfy the constraint 'string'
type BrokenGetters<T> = {
[K in keyof T as `get${Capitalize<K>}`]: () => T[K];
};
// ✅ CORRECTO: acotamiento seguro de la clave
type SafeGetters<T> = {
[K in keyof T as K extends string ? `get${Capitalize<K>}` : never]: () => T[K];
};Emplear Record<keyof T, Value> en lugar de la forma homomórfica [K in keyof T] provoca la pérdida total de metadatos de opcionalidad, forzando todas las propiedades opcionales a convertirse en obligatorias.
Los Mapped Types recursivos no deben penetrar en clases del entorno anfitrión ni en estructuras circulares (HTMLElement, Function, Promise). Asegura siempre condiciones de parada para tipos complejos.
El compilador extrae las claves mediante keyof T, preservando la correlación de índices.
Para cada clave K se ejecuta la expresión de filtrado o transformación de cadenas.
Cualquier clave evaluada a never se descarta de forma atómica de la emisión de propiedades.
Se sustraen (-?) o añaden (+readonly) los modificadores sobre las propiedades conservadas.
Se sintetiza un tipo literal de objeto optimizado para autocompletado en el IDE y validaciones.