Just added: Algorithms you can run and practice

Try it in Practice →

Updated Jun 22, 2026 Edit on GitHub

Generics - Reusable Code That Keeps Its Types

Here's a tension you've already felt. You write a small helper - grab the first element of an array, wrap a value in a box, swap two things - and want it to work for any type: numbers, strings, users, whatever. The lazy way to say "any type" is the any type. But any doesn't mean "any type, tracked" - it means "stop checking entirely." The moment a value passes through any, TypeScript forgets what it was. Your reusable helper becomes a black hole that swallows every type it touches.

A generic is a placeholder for a type, filled in at the moment you call the code. Instead of committing to number or string when you write the function, you leave a blank, and TypeScript fills it with the real type when someone uses it. One function, many types, and the types survive the trip - that's the difference between any (types thrown away) and generics (types carried through).

The problem: any throws the types away

Write the simplest reusable function and watch it fail. first returns the first element of an array. We don't know what's in it, so we reach for any.

function first(arr: any[]): any {
  return arr[0];
}

const n = first([1, 2, 3]);   // n is `any`
const s = first(["a", "b"]);  // s is `any`

n.toUpperCase();  // no error?! n is a number - this crashes at runtime

What just happened: The function works at runtime - it really does return the first element. But look at the types: first([1, 2, 3]) should give a number, first(["a", "b"]) a string. Instead both come back as any, because the return type is annotated any - we told TypeScript "I don't know what this is," and it took us at our word and stopped checking. So n.toUpperCase() - a string method on a number - sails past the type checker and blows up only when the code runs. We threw away exactly the safety we adopted TypeScript for.

⚠️ any is contagious. A single any in a return type spreads to every variable that touches the result, and each of those stops being checked too. Reusable helpers are the worst place for any precisely because they're called everywhere - one can quietly switch off type-checking across half your codebase.

Type parameters: the blank you fill in at the call site

What we actually want: "this function works for some type T, and whatever T goes in, that's what comes out." TypeScript lets you write exactly that with a type parameter.

📝 Type parameter - a named placeholder for a type, in angle brackets after the function name (<T>). It stands in for a real type supplied - usually inferred - when the function is called. By convention a single capital letter (T for "type", K for "key", V for "value"), but any name works.

first again, done right:

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = first([1, 2, 3]);     // n is `number | undefined`
const s = first(["a", "b"]);    // s is `string | undefined`
const u = first<boolean>([]);   // u is `boolean | undefined`

What just happened: <T> declares a type parameter - a blank. The signature reads: "take an array of T, return a T or undefined." Call first([1, 2, 3]) and TypeScript sees a number[] and infers T = number - the return type is number | undefined (the array could be empty, so the element might not be there), so that's what n is. Call it with strings and T becomes string, making s a string | undefined. The same function, written once, produces the correct, specific type at every call site. You almost never write <boolean> explicitly; inference fills T in from the argument.

The payoff isn't only safety - it's tooling. TypeScript now knows n is number | undefined, not any, so your editor red-underlines n.toUpperCase() - a string method a number doesn't have - instantly:

Property 'toUpperCase' does not exist on type 'number'.

💡 The whole win in one line. any says "I don't know and I don't care." A type parameter says "I don't know yet, but I'll remember whatever it turns out to be."

Multiple type parameters

A function can have more than one blank, each independent and inferred separately - like pair, which bundles two possibly-different-typed values into a tuple.

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("id", 42);   // p is [string, number]

What just happened: Two type parameters, A and B. pair("id", 42) infers A = string from the first argument and B = number from the second, so the return type is [string, number]. The two blanks don't have to match - filled independently, exactly what you want for combining unrelated values.

Constraints: extends to require some shape

A bare <T> means "literally any type," sometimes too permissive. A function logging arg.length needs T to have a .length - a number doesn't. Tell TypeScript "T can be any type, as long as it has a length." That's a constraint.

📝 Constraint - a requirement on a type parameter, written <T extends Shape>. It narrows the blank from "any type" to "any type assignable to Shape," letting you safely use Shape's members while still accepting many concrete types.

function logLength<T extends { length: number }>(arg: T): T {
  console.log(arg.length);   // safe: every T is guaranteed to have .length
  return arg;
}

logLength("hello");        // ok, strings have length → logs 5
logLength([1, 2, 3]);      // ok, arrays have length → logs 3
logLength(42);             // error

What just happened: <T extends { length: number }> constrains T to types with a numeric length property. Strings and arrays qualify - and T is still preserved, so logLength([1,2,3]) returns number[], not some flattened type. 42 has no length, so it's rejected at compile time:

Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Note extends here does not mean class inheritance - in a generic constraint it means "is assignable to," "has at least this shape." Same keyword, different job from the extends you'll see with classes next phase.

keyof and safe property access

A common, powerful pattern is reading a property off an object by key without losing track of its type. This needs a second constraint tying one type parameter to another, via keyof.

📝 keyof T - an operator producing the union of T's property names as a literal type. For { name: string; age: number }, keyof of it is "name" | "age". (Phase 10 goes deep on keyof and the rest of the type-operator toolkit.)

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

const user = { name: "Ada", age: 36 };

const name = getProp(user, "name");   // name is `string`
const age = getProp(user, "age");     // age is `number`
getProp(user, "email");               // error: "email" isn't a key of user

What just happened: Two type parameters working together. T is the object's type (inferred as { name: string; age: number }). K extends keyof T constrains key to one of T's actual keys - "name" | "age". The return type T[K] is a lookup: "the type of T's property named K." So getProp(user, "name") returns string and getProp(user, "age") returns number - precise types from one function. Pass a nonexistent key and TypeScript stops you cold:

Argument of type '"email"' is not assignable to parameter of type '"name" | "age"'.

This is generics earning their keep: a single helper that's fully reusable and fully type-safe, catching typo'd property names before the code runs.

Generic types, interfaces, and classes

Generics aren't only for functions. Any type alias, interface, or class can take type parameters too - the same "blank filled in later" idea applied to data shapes.

// A generic interface: a box that holds a value of some type T.
interface Box<T> {
  value: T;
}

const numberBox: Box<number> = { value: 7 };
const stringBox: Box<string> = { value: "hi" };

// A generic type alias: a result that's either success data or an error.
type Result<T> = { ok: true; data: T } | { ok: false; error: string };

function parseAge(input: string): Result<number> {
  const n = Number(input);
  return Number.isNaN(n)
    ? { ok: false, error: "not a number" }
    : { ok: true, data: n };
}

// A generic class: a type-safe stack.
class Stack<T> {
  private items: T[] = [];
  push(item: T): void {
    this.items.push(item);
  }
  pop(): T | undefined {
    return this.items.pop();
  }
}

const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
const top = numbers.pop();   // top is `number | undefined`
numbers.push("oops");        // error: "oops" is not a number

What just happened: Each construct carries a type parameter filled in at the point of use. Box<T> becomes a concrete Box<number> (its value must be a number) or Box<string>. Result<T> is a reusable success-or-error shape - Result<number> here means "on success, data is a number." Stack<T> is a class where T flows through every method: a Stack<number> has push accepting only numbers and pop returning number | undefined. numbers.push("oops") is rejected at compile time - the stack remembers it's a stack of numbers:

Argument of type 'string' is not assignable to parameter of type 'number'.

💡 You've been using generics all along. Every Array<T>, Promise<T>, and Map<K, V> you've written is generic - number[] is shorthand for Array<number>, and Promise<User> is "a promise resolving to a User." The angle-bracket syntax on built-in types is the exact machinery you can now use on your own functions, interfaces, and classes. You were already a fluent user of generics; now you're an author.

Recap

  1. any throws types away; generics carry them through. A reusable helper typed with any stops type-checking everywhere it's used. A generic keeps the real type intact from input to output.
  2. A type parameter (<T>) is a blank filled in at the call site - almost always inferred from the arguments - so one function produces the correct, specific type for every caller.
  3. Type parameters are independent. A function like pair<A, B> can take several, each inferred separately, letting you combine unrelated types without losing either.
  4. Constraints (<T extends Shape>) narrow the blank so you can safely use a shape's members inside the function. <K extends keyof T> plus the lookup type T[K] gives type-safe property access by key.
  5. ⚠️ In a generic constraint, extends means "is assignable to," not class inheritance - "has at least this shape."
  6. Interfaces, type aliases, and classes can be generic too (Box<T>, Result<T>, Stack<T>) - and you already use generic built-ins like Array<T>, Promise<T>, and Map<K, V> constantly.

Next, in Phase 7, from generic containers to classes and OOP - access modifiers, inheritance, interfaces, and where that other meaning of extends shows up.

Quick check

Test yourself on the one idea that drives this whole phase - that a generic keeps the type instead of discarding it:

[
  {
    "q": "Why does `function first<T>(arr: T[]): T | undefined` give a better result than `function first(arr: any[]): any`?",
    "choices": [
      "The generic version infers the element type at the call site, so `first([1,2,3])` returns `number | undefined`, while the `any` version returns `any` and stops type-checking the result",
      "The generic version runs faster because TypeScript optimizes type parameters at runtime",
      "There's no real difference - `T` and `any` mean the same thing",
      "The `any` version is safer because it accepts more argument types"
    ],
    "answer": 0,
    "explain": "A type parameter is inferred from the argument and preserved in the return type, so the caller gets a precise type (`number | undefined`). `any` discards the type and switches off checking, so the result is unchecked `any`. Types are erased at runtime, so there's no speed difference."
  },
  {
    "q": "In `function getProp<T, K extends keyof T>(obj: T, key: K): T[K]`, what does the constraint `K extends keyof T` accomplish?",
    "choices": [
      "It restricts `key` to be one of `T`'s actual property names, so passing a key that doesn't exist is a compile error",
      "It makes `getProp` work only on classes that inherit from `T`",
      "It forces every property of `T` to be a string",
      "It converts `obj` into an array of its keys before returning"
    ],
    "answer": 0,
    "explain": "`keyof T` is the union of `T`'s property names, and `K extends keyof T` constrains `key` to that union - so a non-existent key like `\"email\"` is rejected. The lookup type `T[K]` then returns the precise type of that property."
  },
  {
    "q": "What does `extends` mean in a generic constraint like `<T extends { length: number }>`?",
    "choices": [
      "`T` must be assignable to that shape - i.e. it must have at least a numeric `length` property",
      "`T` must be a subclass of a class named `length`",
      "`T` is automatically given a `length` property if it doesn't have one",
      "`T` can be any type at all, with no restriction"
    ],
    "answer": 0,
    "explain": "In a generic constraint, `extends` means 'is assignable to' - 'has at least this shape.' It lets you safely use `.length` inside the function while still accepting many concrete types (strings, arrays). It is not class inheritance, despite the shared keyword."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. Why does `function first<T>(arr: T[]): T | undefined` give a better result than `function first(arr: any[]): any`?

2. In `function getProp<T, K extends keyof T>(obj: T, key: K): T[K]`, what does the constraint `K extends keyof T` accomplish?

3. What does `extends` mean in a generic constraint like `<T extends { length: number }>`?