Lesson 3 of 5

Generics

In JavaScript, a function that takes "anything" and returns "the same thing" just... works, because nothing is checked either way. The moment you add types, that flexibility seems to disappear — do you write one identity function per type? Generics are TypeScript's answer: a way to write one function or class that works with many types, while still keeping the types connected to each other.

💡 The Bottom Line

A generic is a type parameter — conventionally named T — written in angle brackets right after a function or class name: function identity<T>(value: T): T. It means "whatever type gets passed in for T at the call site, use that exact same type for every other place T appears." You get the reusability of untyped JavaScript back, without losing type safety.

An identity function, with and without generics

JavaScript
function identity(value) {
  return value;
}

identity(5);        // 5
identity("hi");      // "hi"
// works for anything, but nothing
// is checked or preserved
TypeScript (no generic — bad)
function identity(value: any): any {
  return value;
}

let x = identity(5);
x.toUpperCase();
// compiles! any disables all
// checking on the result
TypeScript (generic — correct)
function identity<T>(value: T): T {
  return value;
}

let x = identity(5);
// x is inferred as number

x.toUpperCase();
// compile error: number has no
// method toUpperCase
Same generic, different call
let y = identity("hi");
// y is inferred as string

y.toUpperCase();
// fine — y really is a string

📋 Key differences

  • <T> after the function name declares a type parameter, scoped to that function.
  • T is just a placeholder name — it could be <Item> or <Value>, but T, U, K, V are the common single-letter conventions.
  • You almost never have to write identity<number>(5) explicitly — TypeScript infers T from the argument you pass.
  • Using any instead of a generic also "works" for every type, but it throws away the connection between input and output type entirely — that's the whole difference.

A typed wrapper: generics on a class

Generics show their real value on data structures that hold a value of some type and hand it back later — a box, a stack, a cache entry. In plain JavaScript, such a wrapper accepts and returns anything, so a bug where you push a number into a box of strings won't surface until something downstream calls a string method on it.

JavaScript
class Box {
  constructor(value) {
    this.value = value;
  }
  get() {
    return this.value;
  }
}

const box = new Box("hello");
box.value = 42; // no error, ever
box.get().toUpperCase(); // crashes
                          // at runtime
TypeScript
class Box<T> {
  constructor(private value: T) {}
  get(): T {
    return this.value;
  }
}

const box = new Box<string>("hello");
box.get().toUpperCase(); // fine

const numBox = new Box(42);
// T inferred as number
numBox.get().toUpperCase();
// compile error: number has no
// method toUpperCase

Notice new Box("hello") alone would also infer T as string — the explicit <string> is only needed when TypeScript has nothing to infer from, such as an empty array: new Box<string[]>([]).

⚠️ Gotcha

An unconstrained T gives TypeScript no information about what operations are valid on it — inside a generic function, you can't call value.length or value.toUpperCase() on a plain T, because TypeScript has to allow for T being a number, a boolean, anything. If your generic function needs to rely on a property existing, constrain it: function printLength<T extends { length: number }>(value: T). Forgetting the constraint and reaching for a property anyway is the single most common "why won't this compile" moment when learning generics.

Built-in generics you already use, whether you know it or not

Arrays are themselves a generic type — string[] is shorthand for Array<string>. Once you notice this, generics stop looking like an exotic feature and start looking like the mechanism behind most of the type safety you get for free from the standard library, including Promise<T>, Map<K, V>, and Set<T>.

Shorthand
let names: string[] = ["Ada", "Grace"];
let scores: number[] = [10, 20];
Equivalent generic form
let names: Array<string> = ["Ada", "Grace"];
let scores: Array<number> = [10, 20];

🔧 Try It Yourself

Write a generic function firstOf<T>(items: T[]): T that returns the first element of any array. Call it with a string[] and a number[] and confirm TypeScript infers the correct return type each time. Then try calling .toFixed(2) (a number-only method) on the result when you passed a string[] — confirm the compiler stops you.

🙋 There Are No Dumb Questions

Q: Isn't a generic basically the same thing as any, since both accept any type?
A: They accept any type going in, but they behave completely differently going out. any disables checking on the value forever, at every point it flows through. A generic T keeps track of exactly which type was passed for that specific call, and keeps checking against that type everywhere else T is used in the same call — that link between input and output is exactly what any throws away.

✅ Quick Check

Given function wrap<T>(value: T): T[] { return [value]; }, what is the inferred type of wrap(42)?


Next up: real-world values are often "this OR that" — a response that's either a result or an error, an id that's a string or a number. Union types and narrowing are how TypeScript handles that safely.

← Back to
Next →