Imagine you wrote a small function that takes a value and hands it right back to you. If you write it for a number, it works only for numbers. Write it again for a string, and now you have two nearly identical functions. Do this for every type, and your codebase fills up with copies that all do the same thing. There has to be a better way — and there is. It’s called generics.
Generics are often the moment where TypeScript stops feeling familiar and starts to feel a little intimidating. The angle brackets, the lonely letter T, the extends keyword in a place you didn’t expect — it can look like a different language. But the idea underneath is calm and simple. Once it clicks, you’ll reach for generics naturally, and a lot of TypeScript that used to look cryptic will suddenly make sense. Let’s go slowly and build it from the ground up.
The problem generics solve
Let’s start with the smallest possible example: a function that returns whatever you give it. We’ll call it identity, because it just hands the value straight back.
Here’s a first attempt, written only for numbers:
function identityNumber(value: number): number {
return value;
}
That works, but only for numbers. The moment you need it for a string, you write another one:
function identityString(value: string): string {
return value;
}
You can already feel where this is going. A version for booleans, one for arrays, one for your own User type — the list never ends. So maybe you reach for any, the type that turns type checking off:
function identityAny(value: any): any {
return value;
}
This compiles, and it accepts everything. But it also throws away the one thing you came to TypeScript for. When you call identityAny(42), the result is typed as any, so TypeScript no longer knows it’s a number. You lose autocomplete, you lose error checking, and a typo can slip through unnoticed. You’ve traded safety for flexibility, and that’s a bad trade.
What you actually want is a single function that is flexible and safe. A function that says: “I’ll work with whatever type you pass me, and I promise to give you back that same type.” That promise is exactly what a generic expresses.
Your first generic: a type parameter
Look at this version:
function identity<T>(value: T): T {
return value;
}
There’s one new piece: <T>. That is a type parameter. Just like a regular parameter (value) is a placeholder for a value you’ll supply when you call the function, a type parameter <T> is a placeholder for a type you’ll supply.
The T is just a name. It’s a convention — T for “Type” — but you could call it anything: <Item>, <Value>, <Thing>. TypeScript doesn’t care about the name; it cares that you used the same name in three spots:
function identity<T>(value: T): T
│ │ │
│ │ └─ return type: same T
│ └─────── parameter type: T
└──────────────── declare the placeholder T
Read it as a sentence: “For some type T, this function takes a value of type T and returns a value of type T.” The function doesn’t know or care which type — but it guarantees the type going in matches the type coming out.
Now watch what happens when you call it:
const a = identity<string>("hello"); // a is string
const b = identity<number>(42); // b is number
When you write identity<string>, you’re filling in the placeholder: “this time, T is string.” So value must be a string, and the return type is a string too. No any in sight, full type safety, one function.
You usually don't write the type at all
In most cases you can drop the <string> and just call identity("hello"). TypeScript looks at the argument, sees it’s a string, and figures out that T is string on its own. This is called type inference, and it’s why generics feel lightweight in everyday code.
So the explicit version identity<number>(42) and the short version identity(42) mean the same thing. Spell it out when TypeScript can’t guess, or when you want to be crystal clear; otherwise let inference do the work.
A generic container
A single function is a gentle start. Generics really earn their keep when you build a container — something that holds a value of some type and gives it back later. Think of a labeled box: the box itself is always a box, but what’s inside can be anything, and the label tells you exactly what.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Box<T> is a generic interface. It describes the shape of an object with one property, value, whose type is whatever T you choose. When you write Box<string>, you get a box whose value must be a string. Box<number> gives you one that holds a number. One definition, endless reuse — and each box stays strict about what it carries.
The payoff shows up when you use the contents:
console.log(stringBox.value.toUpperCase()); // OK: value is a string
console.log(numberBox.value.toFixed(2)); // OK: value is a number
TypeScript knows stringBox.value is a string, so it offers string methods like toUpperCase. It knows numberBox.value is a number, so toFixed is available. Try numberBox.value.toUpperCase() and TypeScript stops you before the code ever runs, because numbers don’t have that method. That’s the whole promise of generics: flexibility without losing a single ounce of safety.
You can do the same thing with a type alias instead of an interface, and it reads almost identically:
type Pair<A, B> = {
first: A;
second: B;
};
const coordinate: Pair<number, number> = { first: 10, second: 20 };
const labeled: Pair<string, boolean> = { first: "active", second: true };
Yes — you can have more than one type parameter. Pair<A, B> has two, and you fill in both. This is how built-in types you already use are built. An array of strings is really Array<string>, and a promise that resolves to a User is Promise<User>. You’ve been using generics all along without naming them.
Constraints: limiting what T can be
So far T can be absolutely any type. That’s powerful, but sometimes it’s too loose. Suppose you want a function that reads the length of whatever you pass it:
function logLength<T>(item: T): T {
console.log(item.length); // Error: 'length' does not exist on type 'T'
return item;
}
TypeScript rejects this, and it’s right to. Since T could be anything — a number, a boolean, your own type — there’s no guarantee it has a length property. Numbers don’t. You promised too much flexibility, and now you can’t safely touch .length.
The fix is a constraint. You use the extends keyword to say: “T can still be many types, but it must at least have these properties.” Here we require that T has a length that is a number:
function logLength<T extends { length: number }>(item: T): T {
console.log(item.length); // OK now
return item;
}
logLength("hello"); // OK: a string has length
logLength([1, 2, 3]); // OK: an array has length
logLength({ length: 10 }); // OK: this object has length
logLength(42); // Error: a number has no length
Read T extends { length: number } as “T must be a type that includes at least a length: number property.” Strings qualify, arrays qualify, and any object with a numeric length qualifies. A plain number does not, so TypeScript blocks that call at compile time.
extends here means 'is at least', not 'inherits from'
If you’ve seen extends in classes, it meant inheritance. In a generic constraint it has a related but distinct meaning: “is assignable to” or “is at least this shape.” T extends { length: number } doesn’t mean T is a subclass of anything — it means whatever T turns out to be, it has to fit that shape.
Constraints are what make generics genuinely useful in real code. They let you stay flexible about the type while still relying on the specific parts you actually need.
A practical example: a typed wrapper
Let’s tie it together with something closer to real work — a tiny result wrapper, the kind you might use to carry data through an app along with a status flag.
interface Result<T> {
success: boolean;
data: T;
}
interface User {
id: number;
name: string;
email: string;
}
function wrap<T>(data: T): Result<T> {
return { success: true, data };
}
const userResult = wrap<User>({
id: 1,
name: "Jane Doe",
email: "jane@example.com",
});
console.log(userResult.data.name); // OK: data is a User
console.log(userResult.success); // boolean
Result<T> is a generic interface, and wrap is a generic function that builds one. Because the type parameter flows from the argument all the way into the return type, userResult.data is known to be a User, with name, email, and id all available and checked. Pass a product or an order instead, and the same wrap adapts without a single change. One small piece of code, correct for every type you throw at it.
When to reach for generics
Generics are a tool, not a goal. The instinct to reach for them is simple: use a generic when the same logic should work over many types, and the relationship between an input type and an output type matters.
| Situation | Generic a good fit? |
|---|---|
| The same logic works for many types | Yes |
| Input type determines output type | Yes |
| A container holds a value of varying type | Yes |
| The type is always the same, fixed type | No — just use that type |
| You only need “any object”, shape unknown | Often unknown, not a generic |
If a function only ever handles a User, don’t make it generic — name the type directly and keep it readable. Generics shine when reuse is real and the type connection is worth preserving. Forcing them everywhere makes code harder to read for no benefit.
Recap
Generics let you write one piece of code that works across many types while keeping full type safety — no any, no duplicated functions. Here’s what to carry forward:
- A type parameter like
<T>is a placeholder for a type, filled in when the function or type is used. - The classic
identity<T>(value: T): Tguarantees the type going in matches the type coming out. - Generic interfaces and types (
Box<T>,Pair<A, B>,Result<T>) describe containers that hold a value of any chosen type, and you can have more than one parameter. - Type inference usually lets you skip writing the type explicitly — TypeScript figures
Tout from your arguments. - Constraints with
extendslet you require thatThas certain properties, so you can safely use them.
The leap from “this function works for one type” to “this function works for any type, safely” is the heart of generics. Take the identity and Box examples, type them out yourself, and watch how the types flow. Once that flow feels natural, the rest of TypeScript’s generic machinery — and a huge amount of real-world code — opens right up.