Una guía completa sobre la palabra clave 'infer' de TypeScript, explicando cómo usarla con tipos condicionales para la extracción y manipulación de tipos, incluyendo casos de uso avanzados.
Dominando TypeScript Infer: Extracción de Tipos Condicionales para la Manipulación Avanzada de Tipos
El sistema de tipos de TypeScript es increíblemente potente, lo que permite a los desarrolladores crear aplicaciones robustas y mantenibles. Una de las características clave que permite esta potencia es la palabra clave infer
utilizada junto con tipos condicionales. Esta combinación proporciona un mecanismo para extraer tipos específicos de estructuras de tipos complejas. Esta publicación de blog profundiza en la palabra clave infer
, explicando su funcionalidad y mostrando casos de uso avanzados. Exploraremos ejemplos prácticos aplicables a diversos escenarios de desarrollo de software, desde la interacción con la API hasta la manipulación compleja de estructuras de datos.
¿Qué son los Tipos Condicionales?
Antes de sumergirnos en infer
, revisemos rápidamente los tipos condicionales. Los tipos condicionales en TypeScript te permiten definir un tipo basado en una condición, similar a un operador ternario en JavaScript. La sintaxis básica es:
T extends U ? X : Y
Esto se lee como: "Si el tipo T
es asignable al tipo U
, entonces el tipo es X
; de lo contrario, el tipo es Y
".
Ejemplo:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introducción a la Palabra Clave infer
La palabra clave infer
se usa dentro de la cláusula extends
de un tipo condicional para declarar una variable de tipo que se puede inferir del tipo que se está comprobando. En esencia, te permite "capturar" una parte de un tipo para usarla más tarde.
Sintaxis Básica:
type MyType<T> = T extends (infer U) ? U : never;
En este ejemplo, si T
es asignable a algún tipo, TypeScript intentará inferir el tipo de U
. Si la inferencia tiene éxito, el tipo será U
; de lo contrario, será never
.
Ejemplos Sencillos de infer
1. Inferir el Tipo de Retorno de una Función
Un caso de uso común es inferir el tipo de retorno de una función:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hola, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
En este ejemplo, ReturnType<T>
toma un tipo de función T
como entrada. Verifica si T
es asignable a una función que acepta cualquier argumento y devuelve un valor. Si lo es, infiere el tipo de retorno como R
y lo devuelve. De lo contrario, devuelve any
.
2. Inferir el Tipo de Elemento de un Array
Otro escenario útil es extraer el tipo de elemento de un array:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Aquí, ArrayElementType<T>
verifica si T
es un tipo de array. Si lo es, infiere el tipo de elemento como U
y lo devuelve. Si no, devuelve never
.
Casos de Uso Avanzados de infer
1. Inferir los Parámetros de un Constructor
Puedes usar infer
para extraer los tipos de parámetro de una función constructora:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Persona {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Persona>; // type PersonConstructorParams = [string, number]
class Punto {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Punto>; // type PointConstructorParams = [number, number]
En este caso, ConstructorParameters<T>
toma un tipo de función constructora T
. Infiere los tipos de los parámetros del constructor como P
y los devuelve como una tupla.
2. Extraer Propiedades de Tipos de Objeto
infer
también se puede usar para extraer propiedades específicas de tipos de objeto usando tipos mapeados y tipos condicionales:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface Usuario {
id: number;
nombre: string;
edad: number;
correoElectronico: string;
activo: boolean;
}
type StringProperties = PickByType<Usuario, keyof Usuario, string>; // type StringProperties = { nombre: string; correoElectronico: string; }
type NumberProperties = PickByType<Usuario, keyof Usuario, number>; // type NumberProperties = { id: number; edad: number; }
//Una interfaz que representa coordenadas geográficas.
interface CoordenadasGeograficas {
latitud: number;
longitud: number;
altitud: number;
pais: string;
ciudad: string;
zonaHoraria: string;
}
type NumberCoordinateProperties = PickByType<CoordenadasGeograficas, keyof CoordenadasGeograficas, number>; // type NumberCoordinateProperties = { latitud: number; longitud: number; altitud: number; }
Aquí, PickByType<T, K, U>
crea un nuevo tipo que incluye solo las propiedades de T
(con claves en K
) cuyos valores son asignables al tipo U
. El tipo mapeado itera sobre las claves de T
, y el tipo condicional filtra las claves que no coinciden con el tipo especificado.
3. Trabajando con Promesas
Puedes inferir el tipo resuelto de una Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Datos de la API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
El tipo Awaited<T>
toma un tipo T
, que se espera que sea una Promesa. Luego, el tipo infiere el tipo resuelto U
de la Promesa y lo devuelve. Si T
no es una promesa, devuelve T. Este es un tipo de utilidad incorporado en las versiones más recientes de TypeScript.
4. Extracción del Tipo de un Array de Promesas
Combinar Awaited
y la inferencia de tipo de array te permite inferir el tipo resuelto por un array de Promesas. Esto es particularmente útil cuando se trata con Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Este ejemplo define primero dos funciones asíncronas, getUSDRate
y getEURRate
, que simulan la obtención de tipos de cambio. Luego, el tipo de utilidad PromiseArrayReturnType
extrae el tipo resuelto de cada Promise
en el array, lo que resulta en un tipo de tupla donde cada elemento es el tipo esperado de la Promesa correspondiente.
Ejemplos Prácticos en Diferentes Dominios
1. Aplicación de Comercio Electrónico
Considera una aplicación de comercio electrónico donde obtienes los detalles del producto de una API. Puedes usar infer
para extraer el tipo de los datos del producto:
interface Producto {
id: number;
nombre: string;
precio: number;
descripcion: string;
imagenUrl: string;
categoria: string;
calificacion: number;
paisDeOrigen: string;
}
async function fetchProduct(productId: number): Promise<Producto> {
// Simular llamada a la API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
nombre: 'Producto de Ejemplo',
precio: 29.99,
descripcion: 'Un producto de muestra',
imagenUrl: 'https://example.com/image.jpg',
categoria: 'Electrónicos',
calificacion: 4.5,
paisDeOrigen: 'Canadá'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Producto
function displayProductDetails(product: ProductType) {
console.log(`Nombre del Producto: ${product.nombre}`);
console.log(`Precio: ${product.precio} ${product.paisDeOrigen === 'Canadá' ? 'CAD' : (product.paisDeOrigen === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
En este ejemplo, definimos una interfaz Producto
y una función fetchProduct
que obtiene los detalles del producto de una API. Usamos Awaited
y ReturnType
para extraer el tipo Producto
del tipo de retorno de la función fetchProduct
, lo que nos permite verificar el tipo de la función displayProductDetails
.
2. Internacionalización (i18n)
Supón que tienes una función de traducción que devuelve diferentes cadenas según la configuración regional. Puedes usar infer
para extraer el tipo de retorno de esta función para la seguridad de tipos:
interface Traducciones {
saludo: string;
despedida: string;
mensajeDeBienvenida: (nombre: string) => string;
}
const enTranslations: Traducciones = {
saludo: 'Hello',
despedida: 'Goodbye',
mensajeDeBienvenida: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Traducciones = {
saludo: 'Bonjour',
despedida: 'Au revoir',
mensajeDeBienvenida: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Traducciones {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.mensajeDeBienvenida(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Aquí, el TranslationType
se infiere como la interfaz Traducciones
, asegurando que la función greetUser
tenga la información de tipo correcta para acceder a las cadenas traducidas.
3. Manejo de Respuestas de API
Al trabajar con API, la estructura de la respuesta puede ser compleja. infer
puede ayudar a extraer tipos de datos específicos de respuestas de API anidadas:
interface ApiResponse<T> {
estado: number;
datos: T;
mensaje?: string;
}
interface UserData {
id: number;
nombreDeUsuario: string;
correoElectronico: string;
perfil: {
nombre: string;
apellido: string;
pais: string;
idioma: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simular llamada a la API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
estado: 200,
datos: {
id: userId,
nombreDeUsuario: 'johndoe',
correoElectronico: 'john.doe@example.com',
perfil: {
nombre: 'John',
apellido: 'Doe',
pais: 'USA',
idioma: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['datos']['perfil'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Nombre: ${profile.nombre} ${profile.apellido}`);
console.log(`País: ${profile.pais}`);
}
fetchUser(123).then((response) => {
if (response.estado === 200) {
displayUserProfile(response.datos.perfil);
}
});
En este ejemplo, definimos una interfaz ApiResponse
y una interfaz UserData
. Usamos infer
e indexación de tipos para extraer el UserProfileType
de la respuesta de la API, asegurando que la función displayUserProfile
reciba el tipo correcto.
Mejores Prácticas para Usar infer
- Mantenlo Simple: Usa
infer
solo cuando sea necesario. Usarlo en exceso puede dificultar la lectura y comprensión de tu código. - Documenta Tus Tipos: Agrega comentarios para explicar qué están haciendo tus tipos condicionales y declaraciones
infer
. - Prueba Tus Tipos: Usa la comprobación de tipos de TypeScript para asegurarte de que tus tipos se comportan como se espera.
- Considera el Rendimiento: Los tipos condicionales complejos a veces pueden afectar el tiempo de compilación. Ten en cuenta la complejidad de tus tipos.
- Usa Tipos de Utilidad: TypeScript proporciona varios tipos de utilidad integrados (por ejemplo,
ReturnType
,Awaited
) que pueden simplificar tu código y reducir la necesidad de declaracionesinfer
personalizadas.
Posibles Problemas Comunes
- Inferencia Incorrecta: A veces, TypeScript podría inferir un tipo que no es lo que esperas. Vuelve a comprobar tus definiciones y condiciones de tipo.
- Dependencias Circulares: Ten cuidado al definir tipos recursivos usando
infer
, ya que pueden conducir a dependencias circulares y errores de compilación. - Tipos Demasiado Complejos: Evita crear tipos condicionales demasiado complejos que sean difíciles de entender y mantener. Divídelos en tipos más pequeños y manejables.
Alternativas a infer
Si bien infer
es una herramienta poderosa, existen situaciones en las que los enfoques alternativos podrían ser más apropiados:
- Aserciones de Tipo: En algunos casos, puedes usar aserciones de tipo para especificar explícitamente el tipo de un valor en lugar de inferirlo. Sin embargo, ten cuidado con las aserciones de tipo, ya que pueden omitir la comprobación de tipos.
- Guardias de Tipo: Las guardias de tipo se pueden usar para restringir el tipo de un valor en función de comprobaciones en tiempo de ejecución. Esto es útil cuando necesitas manejar diferentes tipos basados en condiciones en tiempo de ejecución.
- Tipos de Utilidad: TypeScript proporciona un amplio conjunto de tipos de utilidad que pueden manejar muchas tareas comunes de manipulación de tipos sin la necesidad de declaraciones
infer
personalizadas.
Conclusión
La palabra clave infer
en TypeScript, cuando se combina con tipos condicionales, desbloquea capacidades avanzadas de manipulación de tipos. Te permite extraer tipos específicos de estructuras de tipos complejas, lo que te permite escribir código más robusto, mantenible y seguro para tipos. Desde la inferencia de tipos de retorno de funciones hasta la extracción de propiedades de tipos de objetos, las posibilidades son vastas. Al comprender los principios y las mejores prácticas descritas en esta guía, puedes aprovechar infer
al máximo y elevar tus habilidades de TypeScript. Recuerda documentar tus tipos, probarlos a fondo y considerar enfoques alternativos cuando sea apropiado. Dominar infer
te permite escribir código TypeScript verdaderamente expresivo y poderoso, lo que en última instancia conduce a un mejor software.