Español

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

Posibles Problemas Comunes

Alternativas a infer

Si bien infer es una herramienta poderosa, existen situaciones en las que los enfoques alternativos podrían ser más apropiados:

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.