Español

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;
};

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

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.

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

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.