TypeScript Generics Explained
TypeScript generics let functions and types work with any type while preserving the specific type used at each call site, avoiding both duplication and any.
Generics are TypeScript’s mechanism for writing functions, classes, and types that work with a range of types while still preserving the specific type used at each call site. Instead of writing the same function once per type, or falling back to any and losing type safety entirely, a generic function takes a type as a parameter — much like a regular function takes a value as a parameter.
The problem generics solve
Consider a function that returns the first element of an array. Without generics, you have two bad options:
function firstAny(arr: any[]): any {
return arr[0];
}
This compiles for any array, but the return type is any — TypeScript has no idea what’s actually inside, so it can’t catch a typo on the next line that assumes the wrong shape. The alternative is writing a separate function per type (firstString, firstNumber, firstUser), which duplicates logic that’s identical except for the type involved.
Generics solve this by letting the type itself be a parameter:
function first<T>(arr: T[]): T {
return arr[0];
}
const num = first([1, 2, 3]); // inferred as number
const name = first(["a", "b", "c"]); // inferred as string
T is a type variable — a placeholder filled in at each call site. TypeScript infers T from the argument automatically in most cases, so callers don’t need to specify it explicitly, though you can with first<string>(["a", "b"]) when inference isn’t enough.
Constraining generics with extends
An unconstrained T can be anything, which means you can’t assume it has any particular property. Constraints narrow that down:
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // works — strings have .length
getLength([1, 2, 3]); // works — arrays have .length
getLength(42); // error — numbers don't have .length
T extends { length: number } says “T can be any type, as long as it has a length property that’s a number.” This is the core tool for balancing flexibility against safety: as generic as possible, as constrained as necessary.
Generic interfaces and classes
Generics apply to types, not just functions. A generic interface can describe a container shape without committing to what it contains:
interface Box<T> {
value: T;
unwrap(): T;
}
const numberBox: Box<number> = { value: 42, unwrap: () => 42 };
This pattern shows up constantly in real code — a Promise<T> resolves to a value of type T, an ORM’s query builder is often generic over the row type it returns, and state management libraries are frequently generic over the shape of application state. See what an ORM is for more on how database libraries use this kind of type parameterization to give you typed query results without hand-writing types for every table.
Multiple type parameters
Generics aren’t limited to one type variable. A function that pairs two values might need both:
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair("id", 42); // [string, number]
This shows up often in utility functions that combine or transform two independently-typed inputs, such as merging two objects or zipping two arrays together.
Generics vs any vs unknown
It’s worth being explicit about how these three relate, since beginners often reach for any when a generic is the better tool:
any | unknown | Generic (T) | |
|---|---|---|---|
| Type safety | None — disables checking entirely | Safe, but must narrow before use | Full — the specific type is tracked |
| Return type reflects input | No | No | Yes |
| Best for | Escaping the type system (avoid where possible) | Values of genuinely unknown shape | Reusable code that should stay type-safe |
any should be a last resort, not a default. If you find yourself typing any because you don’t want to write a type, a generic is very often the fix — it gets you the same flexibility without giving up compiler checking.
Where to learn more
Generics are one of the features that most separates “using TypeScript” from “writing JavaScript with type annotations sprinkled on top.” If you’re still getting comfortable with the basics, getting started with TypeScript covers the fundamentals generics build on. And because generics are a compile-time-only construct — they leave no trace in the emitted JavaScript — it’s worth understanding what JavaScript is underneath, so it’s clear generics are purely a development-time safety net, not something with any runtime cost or behavior.
The takeaway
Generics let you write one function, class, or interface that works across many types while keeping the specific type tracked at each call site — the best of both worlds between duplicated per-type code and unsafe any. Start with an unconstrained type parameter, add extends constraints only when the function genuinely needs to assume something about the type’s shape, and reach for a generic before reaching for any whenever code needs to stay flexible without giving up type safety.
Keep reading
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 3 min read TypeScript's never Type, Explained
never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.
Takina · · 4 min read TypeScript readonly Modifiers Explained
TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.