Una guía completa sobre los potentes Tipos Mapeados y Tipos Condicionales de TypeScript, con ejemplos prácticos y casos de uso avanzados para crear aplicaciones robustas y con seguridad de tipos.
Dominando los Tipos Mapeados y Tipos Condicionales de TypeScript
TypeScript, un superconjunto de JavaScript, ofrece potentes características para crear aplicaciones robustas y mantenibles. Entre estas características, los Tipos Mapeados y los Tipos Condicionales se destacan como herramientas esenciales para la manipulación avanzada de tipos. Esta guía proporciona una visión general completa de estos conceptos, explorando su sintaxis, aplicaciones prácticas y casos de uso avanzados. Ya sea que seas un desarrollador de TypeScript experimentado o estés comenzando tu viaje, este artículo te equipará con el conocimiento para aprovechar estas características de manera efectiva.
¿Qué son los Tipos Mapeados?
Los Tipos Mapeados te permiten crear nuevos tipos transformando los existentes. Iteran sobre las propiedades de un tipo existente y aplican una transformación a cada propiedad. Esto es particularmente útil para crear variaciones de tipos existentes, como hacer que todas las propiedades sean opcionales o de solo lectura.
Sintaxis Básica
La sintaxis para un Tipo Mapeado es la siguiente:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: El tipo de entrada que quieres mapear.K in keyof T
: Itera sobre cada clave en el tipo de entradaT
.keyof T
crea una unión de todos los nombres de propiedad enT
, yK
representa cada clave individual durante la iteración.Transformation
: La transformación que deseas aplicar a cada propiedad. Esto podría ser agregar un modificador (comoreadonly
o?
), cambiar el tipo o algo completamente diferente.
Ejemplos Prácticos
Hacer Propiedades de Solo Lectura
Digamos que tienes una interfaz que representa un perfil de usuario:
interface UserProfile {
name: string;
age: number;
email: string;
}
Puedes crear un nuevo tipo donde todas las propiedades son de solo lectura:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Ahora, ReadOnlyUserProfile
tendrá las mismas propiedades que UserProfile
, pero todas serán de solo lectura.
Hacer Propiedades Opcionales
De manera similar, puedes hacer que todas las propiedades sean opcionales:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
tendrá todas las propiedades de UserProfile
, pero cada propiedad será opcional.
Modificar Tipos de Propiedad
También puedes modificar el tipo de cada propiedad. Por ejemplo, puedes transformar todas las propiedades para que sean cadenas de texto (strings):
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
En este caso, todas las propiedades en StringifiedUserProfile
serán de tipo string
.
¿Qué son los Tipos Condicionales?
Los Tipos Condicionales te permiten definir tipos que dependen de una condición. Proporcionan una forma de expresar relaciones de tipo basadas en si un tipo satisface una restricción particular. Esto es similar a un operador ternario en JavaScript, pero para tipos.
Sintaxis Básica
La sintaxis para un Tipo Condicional es la siguiente:
T extends U ? X : Y
T
: El tipo que se está verificando.U
: El tipo queT
extiende (la condición).X
: El tipo a devolver siT
extiendeU
(la condición es verdadera).Y
: El tipo a devolver siT
no extiendeU
(la condición es falsa).
Ejemplos Prácticos
Determinar si un Tipo es un String
Vamos a crear un tipo que devuelva string
si el tipo de entrada es un string, y number
en caso contrario:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Extraer un Tipo de una Unión
Puedes usar tipos condicionales para extraer un tipo específico de un tipo de unión. Por ejemplo, para extraer tipos no nulos (non-nullable):
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
Aquí, si T
es null
o undefined
, el tipo se convierte en never
, que luego es filtrado por la simplificación de tipos de unión de TypeScript.
Inferir Tipos
Los tipos condicionales también se pueden usar para inferir tipos usando la palabra clave infer
. Esto te permite extraer un tipo de una estructura de tipo más compleja.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
En este ejemplo, ReturnType
extrae el tipo de retorno de una función. Comprueba si T
es una función que toma cualquier argumento y devuelve un tipo R
. Si lo es, devuelve R
; de lo contrario, devuelve any
.
Combinando Tipos Mapeados y Tipos Condicionales
El verdadero poder de los Tipos Mapeados y los Tipos Condicionales proviene de su combinación. Esto te permite crear transformaciones de tipo altamente flexibles y expresivas.
Ejemplo: Deep Readonly (Solo Lectura Profundo)
Un caso de uso común es crear un tipo que haga que todas las propiedades de un objeto, incluidas las anidadas, sean de solo lectura. Esto se puede lograr usando un tipo condicional recursivo.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
Aquí, DeepReadonly
aplica recursivamente el modificador readonly
a todas las propiedades y sus propiedades anidadas. Si una propiedad es un objeto, llama recursivamente a DeepReadonly
sobre ese objeto. De lo contrario, simplemente aplica el modificador readonly
a la propiedad.
Ejemplo: Filtrar Propiedades por Tipo
Digamos que quieres crear un tipo que solo incluya propiedades de un tipo específico. Puedes combinar Tipos Mapeados y Tipos Condicionales para lograrlo.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
En este ejemplo, FilterByType
itera sobre las propiedades de T
y comprueba si el tipo de cada propiedad extiende U
. Si es así, incluye la propiedad en el tipo resultante; de lo contrario, la excluye mapeando la clave a never
. Observa el uso de "as" para remapear claves. Luego usamos `Omit` y `keyof StringProperties` para eliminar las propiedades de tipo string de la interfaz original.
Casos de Uso y Patrones Avanzados
Más allá de los ejemplos básicos, los Tipos Mapeados y los Tipos Condicionales se pueden usar en escenarios más avanzados para crear aplicaciones altamente personalizables y con seguridad de tipos.
Tipos Condicionales Distributivos
Los tipos condicionales son distributivos cuando el tipo que se está verificando es un tipo de unión. Esto significa que la condición se aplica a cada miembro de la unión individualmente, y los resultados se combinan en un nuevo tipo de unión.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
En este ejemplo, ToArray
se aplica a cada miembro de la unión string | number
individualmente, resultando en string[] | number[]
. Si la condición no fuera distributiva, el resultado habría sido (string | number)[]
.
Uso de Tipos de Utilidad (Utility Types)
TypeScript proporciona varios tipos de utilidad integrados que aprovechan los Tipos Mapeados y los Tipos Condicionales. Estos tipos de utilidad se pueden usar como bloques de construcción para transformaciones de tipo más complejas.
Partial<T>
: Hace que todas las propiedades deT
sean opcionales.Required<T>
: Hace que todas las propiedades deT
sean obligatorias.Readonly<T>
: Hace que todas las propiedades deT
sean de solo lectura.Pick<T, K>
: Selecciona un conjunto de propiedadesK
deT
.Omit<T, K>
: Elimina un conjunto de propiedadesK
deT
.Record<K, T>
: Construye un tipo con un conjunto de propiedadesK
de tipoT
.Exclude<T, U>
: Excluye deT
todos los tipos que son asignables aU
.Extract<T, U>
: Extrae deT
todos los tipos que son asignables aU
.NonNullable<T>
: Excluyenull
yundefined
deT
.Parameters<T>
: Obtiene los parámetros de un tipo de funciónT
.ReturnType<T>
: Obtiene el tipo de retorno de un tipo de funciónT
.InstanceType<T>
: Obtiene el tipo de instancia de un tipo de función constructoraT
.
Estos tipos de utilidad son herramientas poderosas que pueden simplificar manipulaciones de tipo complejas. Por ejemplo, puedes combinar Pick
y Partial
para crear un tipo que haga que solo ciertas propiedades sean opcionales:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
En este ejemplo, OptionalDescriptionProduct
tiene todas las propiedades de Product
, pero la propiedad description
es opcional.
Uso de Tipos de Plantillas Literales (Template Literal Types)
Los Tipos de Plantillas Literales te permiten crear tipos basados en literales de cadena. Se pueden usar en combinación con Tipos Mapeados y Tipos Condicionales para crear transformaciones de tipo dinámicas y expresivas. Por ejemplo, puedes crear un tipo que prefije todos los nombres de propiedad con una cadena específica:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
En este ejemplo, PrefixedSettings
tendrá las propiedades data_apiUrl
y data_timeout
.
Mejores Prácticas y Consideraciones
- Mantenlo Simple: Aunque los Tipos Mapeados y los Tipos Condicionales son poderosos, también pueden hacer que tu código sea más complejo. Intenta mantener tus transformaciones de tipo lo más simples posible.
- Usa Tipos de Utilidad: Aprovecha los tipos de utilidad integrados de TypeScript siempre que sea posible. Están bien probados y pueden simplificar tu código.
- Documenta Tus Tipos: Documenta claramente tus transformaciones de tipo, especialmente si son complejas. Esto ayudará a otros desarrolladores a entender tu código.
- Prueba Tus Tipos: Usa la verificación de tipos de TypeScript para asegurar que tus transformaciones de tipo funcionen como se espera. Puedes escribir pruebas unitarias para verificar el comportamiento de tus tipos.
- Considera el Rendimiento: Las transformaciones de tipo complejas pueden afectar el rendimiento de tu compilador de TypeScript. Sé consciente de la complejidad de tus tipos y evita cálculos innecesarios.
Conclusión
Los Tipos Mapeados y los Tipos Condicionales son características poderosas en TypeScript que te permiten crear transformaciones de tipo altamente flexibles y expresivas. Al dominar estos conceptos, puedes mejorar la seguridad de tipos, la mantenibilidad y la calidad general de tus aplicaciones de TypeScript. Desde transformaciones simples como hacer propiedades opcionales o de solo lectura hasta transformaciones recursivas complejas y lógica condicional, estas características proporcionan las herramientas que necesitas para construir aplicaciones robustas y escalables. Sigue explorando y experimentando con estas características para desbloquear todo su potencial y convertirte en un desarrollador de TypeScript más competente.
A medida que continúas tu viaje con TypeScript, recuerda aprovechar la gran cantidad de recursos disponibles, incluida la documentación oficial de TypeScript, las comunidades en línea y los proyectos de código abierto. Adopta el poder de los Tipos Mapeados y los Tipos Condicionales, y estarás bien equipado para abordar incluso los problemas más desafiantes relacionados con los tipos.