← React & TypeScript

TypeScript Fundamentals

What are generics in TypeScript and what are they for?

Show answer — try answering out loud first

Generics are type parameters: they let you write functions and structures that work with several types without losing the relationship between input and output. If you pass a string[] to primerElemento, the compiler knows it returns string, not a generic any.

Imagine a function that returns the first element of an array. Without generics you have two bad options: type it with any (you lose all safety) or write a version for each type (you duplicate code). Generics offer the third way:

  • <T> is a type parameter: like regular parameters, but for types. It gets "filled in" on each call, almost always by inference.
  • They preserve the input → output relationship: function primero<T>(arr: T[]): T promises to return the same type the array contains.
  • Constraints with extends: <T extends { id: number }> means "any type, as long as it has a numeric id". Inside the function you can safely use .id.
  • K extends keyof T: the star pattern. K can only be a real key of T, so accessing objeto[clave] is safe and the return is typed as T[K].

In React you use them without noticing: useState<number>(0) is a generic hook.

// Without generics the type is lost:
// function primerElemento(arr: any[]): any  ->  the output is any

// With generics, the input -> output relationship is preserved
function primerElemento<T>(arr: T[]): T | undefined {
  return arr[0];
}

const numero = primerElemento([10, 20, 30]);      // number | undefined
const palabra = primerElemento(["a", "b", "c"]);  // string | undefined
// numero.toUpperCase();
// Error: Property 'toUpperCase' does not exist on type 'number'.

// Constraint with extends: T must have a length
function masLargo<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

masLargo("hola", "adiós");     // works: string has length
masLargo([1, 2], [1, 2, 3]);   // works: arrays have length
// masLargo(10, 20);
// Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

// K extends keyof T: only accepts real keys of the object
function extraerPropiedad<T, K extends keyof T>(objeto: T, clave: K): T[K] {
  return objeto[clave];
}

const usuario = { nombre: "Ana", edad: 30 };
const nombre = extraerPropiedad(usuario, "nombre"); // string
const edad = extraerPropiedad(usuario, "edad");     // number
// extraerPropiedad(usuario, "email");
// Error: Argument of type '"email"' is not assignable to parameter of type '"nombre" | "edad"'.

Using any where a generic belonged, "because hey, it works with everything":

// Compiles, but the output loses the type
function primeroMal(arr: any[]): any {
  return arr[0];
}
const x = primeroMal([1, 2, 3]);
x.toUpperCase(); // compiles and blows up at runtime: x is any

// Fix: the generic preserves the type
function primeroBien<T>(arr: T[]): T | undefined {
  return arr[0];
}
const y = primeroBien([1, 2, 3]); // number | undefined

The key difference: any accepts everything and forgets everything; the generic accepts everything and remembers the type.

"A generic is a type parameter: it lets me write a single function that works with many types without losing the relationship between input and output. For example, primerElemento<T>(arr: T[]): T returns number if I pass it numbers and string if I pass it strings, something I'd lose with any. I can constrain the generic with extends, and the pattern I use most is K extends keyof T to access an object's properties safely. In React I use them daily: useState<T> is a generic hook."

Quick challenge

Write ultimoElemento<T> that returns the last element of an array (or undefined if it's empty), and verify that with ["a", "b"] the result is typed as string | undefined.

See solution
function ultimoElemento<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const letra = ultimoElemento(["a", "b"]);   // string | undefined
const numero = ultimoElemento([1, 2, 3]);   // number | undefined
const vacio = ultimoElemento<number>([]);   // undefined at runtime

// The relationship is preserved: if letra exists, it's a string
if (letra !== undefined) {
  console.log(letra.toUpperCase()); // "B"
}