Menu

Restricciones genéricas en TypeScript: extends y keyof T

Una restricción genérica, T extends Something, limita qué tipos acepta un parámetro de tipo y permite que la función use lo que la restricción garantiza. Incluye extends con formas de objeto e interfaces, K extends keyof T para acceder a propiedades con seguridad, restricciones sobre primitivos y los errores que te vas a encontrar.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una restricción genérica limita lo que puede ser un parámetro de tipo. <T extends { length: number }> solo acepta tipos que tengan un length numérico (strings, arrays, tus propios objetos), y gracias a esa promesa la función puede leer .length.

El tipo de retorno sigue siendo T, no { length: number }: si pasas dos arrays de números, recibes number[], y los objetos conservan su tag. La restricción describe el mínimo; el tipo de quien llama se conserva.

Por qué hacen falta las restricciones

Dentro de una función genérica, un T sin restricción podría ser cualquier cosa: un string, un número, null, una función. Así que TypeScript solo permite lo que funciona con todos los tipos, que es casi nada.

El compilador muestra:

index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.

La solución es decir lo que necesitas: <T extends { length: number }>. Las restricciones convierten «cualquier tipo» en «cualquier tipo que tenga esto», que suele ser lo que quiere decir una función genérica.

Restringir a una interfaz

La restricción puede ser cualquier tipo, incluida una interfaz o un alias de tipo que ya tengas. La función funciona entonces con cualquier cosa que tenga al menos esos miembros, y sigue devolviendo el tipo completo de quien llama.

Compáralo con un tipo de parámetro normal: function byId(items: HasId[], id: number): HasId | undefined acepta los mismos arrays, pero el resultado es solo HasId, así que found.name sería un error. La versión genérica deja pasar el tipo completo.

K extends keyof T: acceso seguro a propiedades

La restricción más habitual relaciona dos parámetros de tipo. K extends keyof T dice que K debe ser uno de los nombres de propiedad de T, y T[K] es el tipo de esa propiedad.

Una clave mal escrita o que no existe es un error de compilación, y el tipo de retorno sigue a la clave: "year" da number, "title" da string. La página de keyof explica el propio keyof, incluido cómo se comporta con las firmas de índice.

Restringir a primitivos

Una restricción puede ser un primitivo o una unión de primitivos. Un efecto secundario útil: cuando T extends string (o number), TypeScript infiere el tipo literal del argumento en lugar de ensancharlo.

Sin la restricción, createEvent("user.saved") devuelve { type: string }: aquí TypeScript ensancha a string el literal que infirió para T. Con T extends string el literal se conserva, y así es como las funciones auxiliares tipadas de eventos y rutas obtienen nombres exactos a partir de argumentos string normales.

Errores habituales

Devolver algo que solo encaja con la restricción. Una función que devuelve T debe devolver un T, no cualquier valor que encaje con la restricción:

interface HasId {
  id: number;
}

function reset<T extends HasId>(item: T): T {
  return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
//   '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
//   instantiated with a different subtype of constraint 'HasId'.

Si T es { id: number; name: string }, el objeto nuevo no tiene name, así que no es un T. Devuelve una expansión de la entrada, return { ...item, id: 0 };, que conserva todas las propiedades, o declara el tipo de retorno como HasId si eso es todo lo que produces.

Pasar un argumento de tipo que incumple la restricción. Los argumentos de tipo explícitos también se comprueban: con function scale<T extends number>(x: T), la llamada scale<string>("2") falla con Type 'string' does not satisfy the constraint 'number'. (TS2344).

Restringir cuando no hace falta un genérico. Si T solo aparece en el parámetro, function print<T extends HasId>(item: T): void es simplemente function print(item: HasId): void con sintaxis extra. Las restricciones importan cuando T también aparece en el tipo de retorno o en otro parámetro.

Restricciones con valores por defecto

Un parámetro de tipo puede tener a la vez una restricción y un valor por defecto. El valor por defecto debe cumplir la restricción.

interface Store<TState extends object = Record<string, unknown>> {
  get(): TState;
  set(next: Partial<TState>): void;
}

type AnyStore = Store;                              // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>;       // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.

La misma palabra clave extends aparece también en los conditional types, T extends string ? A : B, donde es una prueba y no una restricción; la página de conditional types trata esa forma.

Preguntas frecuentes

¿Qué significa T extends en un genérico de TypeScript?

<T extends Constraint> significa que T puede ser cualquier tipo que se pueda asignar a Constraint. Quien llama solo puede pasar esos tipos, y dentro de la función puedes usar todo lo que garantiza la restricción. <T extends { length: number }> acepta strings, arrays y cualquier objeto con un length numérico, y te deja leer value.length.

¿Qué significa K extends keyof T?

K debe ser uno de los nombres de propiedad de T. Junto con el indexed access type T[K] tipa con exactitud el acceso a propiedades: function get<T, K extends keyof T>(obj: T, key: K): T[K] solo acepta claves reales de obj, y el tipo de retorno es el tipo de esa propiedad.

¿Por qué me sale "Property does not exist on type T"?

Un T sin restricción podría ser cualquier tipo, incluidos number o null, así que TypeScript no permite nada específico de un tipo sobre él (TS2339). Añade una restricción que prometa la propiedad: <T extends { name: string }>, o restringe a una interfaz que ya tengas, <T extends User>.

¿Qué significa "T could be instantiated with a different subtype of constraint"?

Es el error TS2322: la función promete devolver T, pero devuelve un valor que solo encaja con la restricción. Si T es { id: number; name: string }, un { id: 1 } nuevo no es un T. Devuelve el valor que recibiste (o una expansión de él, tipada en consecuencia), o cambia el tipo de retorno a la restricción.

¿Qué diferencia hay entre extends en un genérico y extends en una clase o interfaz?

Misma palabra clave, idea relacionada. En class Dog extends Animal e interface B extends A declara herencia. En <T extends A> declara una restricción: T debe poder asignarse a A. En un conditional type, T extends A ? X : Y, es una prueba que elige uno de dos tipos.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR