infer avanzado en TypeScript: patrones de extracción, UnpackPromise, Head/Tail de tuplas y más

infer es la herramienta que convierte los tipos condicionales de TypeScript en algo realmente potente: permite capturar partes de un tipo dentro de la cláusula extends y usarlas en la rama verdadera. Dominar sus patrones es la clave para implementar tipos de utilidad propios al nivel de los que incluye la librería estándar.

infer básico: extraer tipos de funciones

// ReturnType implementado desde cero:
type MiReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MiReturnType<() => string>;            // string
type R2 = MiReturnType<(x: number) => boolean>; // boolean

// Parameters implementado desde cero:
type MiParameters<T> = T extends (...args: infer P) => any ? P : never;

type P1 = MiParameters<(a: string, b: number) => void>; // [string, number]

// ConstructorParameters:
type MiConstructorParams<T> =
  T extends abstract new (...args: infer P) => any ? P : never;

class Punto { constructor(public x: number, public y: number) {} }
type CP = MiConstructorParams<typeof Punto>; // [number, number]

Head y Tail de tuplas

Con infer y los rest elements se pueden extraer el primer elemento y el resto de una tupla:

type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer T] ? T : never;

type H = Head<[string, number, boolean]>; // string
type T = Tail<[string, number, boolean]>; // [number, boolean]
type T2 = Tail<[string]>;                 // []
type T3 = Tail<[]>;                       // never

// Último elemento de la tupla:
type Last<T extends unknown[]> =
  T extends [...unknown[], infer L] ? L : never;

type L = Last<[string, number, boolean]>; // boolean

UnpackPromise y Awaited

// Una sola capa:
type UnpackPromise<T> = T extends Promise<infer V> ? V : T;

// Recursiva (equivale al tipo Awaited de la librería estándar):
type MiAwaited<T> =
  T extends null | undefined
    ? T
    : T extends PromiseLike<infer V>
    ? MiAwaited<V>
    : T;

type A1 = MiAwaited<Promise<string>>;                  // string
type A2 = MiAwaited<Promise<Promise<number>>>;          // number
type A3 = MiAwaited<boolean>;                           // boolean

Múltiples infer en una condición

Una sola cláusula extends puede capturar varios tipos a la vez:

// Separar primer argumento y resto de los parámetros:
type PrimerArg<T> = T extends (first: infer F, ...rest: infer R) => any
  ? [first: F, rest: R]
  : never;

type PA = PrimerArg<(x: string, y: number, z: boolean) => void>;
// [first: string, rest: [number, boolean]]

// Extraer key y value de una entrada de un objeto:
type EntryOf<T> = T extends [infer K, infer V] ? { clave: K; valor: V } : never;
type E = EntryOf<['nombre', string]>; // { clave: 'nombre'; valor: string }

infer con Template Literal Types

// Extraer el nombre de un getter:
type GetterName<T extends string> =
  T extends `get${infer Nombre}` ? Nombre : never;

type G = GetterName<'getNombre'>;    // 'Nombre'
type G2 = GetterName<'setNombre'>;   // never

// Quitar prefijo 'on' de eventos:
type EventName<T extends string> =
  T extends `on${infer E}` ? Uncapitalize<E> : never;

type Ev = EventName<'onClick'>;  // 'click'

// Convertir snake_case a camelCase recursivo:
type CamelCase<S extends string> =
  S extends `${infer Head}_${infer Rest}`
    ? `${Lowercase<Head>}${Capitalize<CamelCase<Rest>>}`
    : Lowercase<S>;

type CC = CamelCase<'nombre_completo_usuario'>; // 'nombreCompletoUsuario'

Combinar infer con distribución

// Extraer el tipo de los valores de un objeto:
type ValoresDeObjeto<T> = T extends Record<string, infer V> ? V : never;

type Valores = ValoresDeObjeto<{ a: string; b: number; c: boolean }>;
// string | number | boolean

// Obtener los tipos de los elementos de un array de tuplas:
type ElementosDeTuplas<T extends unknown[][]> =
  T extends Array<infer E> ? E[number] : never;

Imagen: Pexels / Pixabay

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP