Kodokon kodokon.com

Genéricos avanzados: restricciones y valores por defecto

Restringes tus parámetros de tipo con extends y proporcionas valores por defecto para construir API genéricas ergonómicas.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Un genérico sin restricción es opaco: dentro del cuerpo de la función, T no te deja hacer nada. La cláusula extends establece un contrato mínimo sobre el parámetro de tipo, lo que desbloquea el acceso a las propiedades garantizadas conservando a la vez el tipo preciso de quien llama. Esa es la diferencia fundamental con un parámetro tipado con una interfaz: la restricción conserva la información, la interfaz la borra.

TYPESCRIPT
function longest<T extends { length: number }>(
  a: T,
  b: T
): T {
  return a.length >= b.length ? a : b;
}

const word = longest("hello", "worldwide");
const list = longest([1, 2], [3, 4, 5]);
word conserva el tipo string, list el tipo number[].

El dúo K extends keyof T es el patrón cotidiano más rentable: enlaza dos parámetros de tipo entre sí y garantiza estáticamente que una clave pertenece realmente al objeto. El tipo de retorno T[K] (un tipo de acceso indexado) sigue con precisión la propiedad solicitada. Así es exactamente como se tipan funciones como pick o pluck de las librerías de utilidades.

TYPESCRIPT
function getProp<T extends object, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const config = { port: 8080, host: "localhost" };
const port = getProp(config, "port");
const host = getProp(config, "host");
port se infiere como number, host como string; cualquier otra clave se rechaza.

Los valores por defecto (T = string) hacen que un genérico sea opcional en el sitio de la llamada: los usuarios solo proporcionan el argumento de tipo cuando quieren desviarse del caso común. Combinados con una restricción (T extends X = Y), definen a la vez el límite y el comportamiento implícito. El valor por defecto debe satisfacer la restricción, de lo contrario la propia declaración no compila. Es el patrón estándar para clientes HTTP, stores y emisores de eventos tipados.

TYPESCRIPT
interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}

async function fetchJson<T = unknown>(
  url: string
): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return { status: res.status, data: await res.json() };
}

interface Post { id: number; title: string }
const typed = await fetchJson<Post>("/api/posts/1");
const loose = await fetchJson("/api/health");
Sin argumento de tipo, data recae en unknown, no en any.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué garantiza la restricción K extends keyof T en una firma genérica?
    • Que K es cualquier cadena de texto
    • Que K es una clave existente de T, verificada en tiempo de compilación
    • Que T contiene al menos una clave opcional
    • Que K y T son el mismo tipo
  2. ¿Qué ventaja tiene longest<T extends { length: number }> sobre longest(a: { length: number }, b: ...) sin genéricos?
    • Se ejecuta más rápido
    • Conserva el tipo preciso de quien llama en el valor de retorno
    • Acepta más tipos de entrada
  3. ¿Por qué preferir <T = unknown> en lugar de <T = any> como valor por defecto?
    • unknown compila más rápido que any
    • any está prohibido en modo estricto
    • unknown obliga a quien llama a reducir el tipo antes de usarlo, any desactiva toda verificación
    • unknown acepta menos valores que any