Saltar al contenido principal
Volver atrás

TypeScript #9: Generics Intermedios (Constraints)

#typescript #generics #constraints

Cómo domar a los Generics para que no acepten "cualquier cosa". Constraints, valores por defecto y el truco de keyof.

En la parte anterior vimos que <T> puede ser cualquier cosa. A veces, eso es demasiado flexible. ¿Qué pasa si queremos que T tenga sí o sí una propiedad .length?

Restricciones (extends)

Imaginemos esta función:

typescript
function obtenerLongitud<T>(item: T) {
  return item.length; // ❌ Error: Property 'length' does not exist on type 'T'.
}

// TS se queja porque T podría ser un número (que no tiene .length), 
// o un boolean...

Para arreglarlo, usamos extends para poner una restricción (Constraint).

typescript
// Le decimos: "T puede ser lo que quieras, PERO debe extender (cumplir) esta forma"
interface TieneLongitud {
  length: number;
}

function obtenerLongitud<T extends TieneLongitud>(item: T) {
  return item.length; // ✅ Ahora TS sabe que existe
}

obtenerLongitud("Hola"); // ✅ string tiene .length
obtenerLongitud([1, 2, 3]); // ✅ array tiene .length
obtenerLongitud(123); // ❌ Error: number no tiene .length

Generics con keyof (El truco maestro)

Este patrón lo veremos en todas partes. Queremos una función que obtenga una propiedad de un objeto de forma segura.

Necesitamos definir dos Generics:

  1. T: El objeto.
  2. K: La clave (key). Pero K no puede ser cualquier string, debe ser una key de T.
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const developer = {
  name: "BartoloDev",
  language: "TypeScript"
};

getProperty(developer, "name"); // ✅ TS sabe que esto devuelve string
getProperty(developer, "age"); // ❌ Error: "age" no es una key de developer

Valores por Defecto (=)

Al igual que en las funciones normales (function suma(a, b = 0)), los Generics pueden tener valores por defecto.

Esto es muy útil para no obligar al usuario a escribir el tipo siempre.

typescript
// Si no pasamos T, asumiré que es 'string'
interface RespuestaAPI<T = string> {
  data: T;
  error: boolean;
}

// Uso sin pasar tipo (usa el default)
const respuestaSimple: RespuestaAPI = {
  data: "Todo ok", // T es string
  error: false
};

// Uso especificando tipo
const respuestaCompleja: RespuestaAPI<number> = {
  data: 200, // T es number
  error: false
};

¡Probémoslo!

Creemos una función fusionar que tome dos objetos obj1 y obj2 y los combine. Tipemos la función usando dos generics T y U.

En la Parte 8, abordaremos una de las mayores confusiones: Generics en Interfaces vs en Tipos, y cómo pasar tipos de padres a hijos (prop drilling de tipos).