Getting Started with Generics
Discover how generics make TypeScript code reusable, precise, and safe across functions, types, interfaces, and classes.
We write a lot of functions that are meant to be flexible: loggers, wrappers, utilities, and state containers. In JavaScript, flexibility comes at the cost of safety. But in TypeScript, generics give us both—the ability to adapt to any type while still being type-safe.
Generics are how TypeScript lets us write reusable patterns without losing information about the types we’re working with. They let us define behavior once and apply it across many different types cleanly.
Let’s start by writing the simplest possible generic function.
Writing your first generic function
Here’s a basic identity function. It takes a value and returns it unchanged, but with full type fidelity.
function identity<T>(value: T): T {return value;}const str = identity("hello"); // type: stringconst num = identity(42); // type: numberconsole.log(`String: ${str}, Number: ${num}`);
Explanation:
Line 1: We define a type parameter
T
inside angle brackets. This is a placeholder for any type. The function accepts a parametervalue
of typeT
, and returns a value of the same typeT
.Lines 5–6: TypeScript infers the type for
T
based on the argument passed—no manual annotations needed.
This is the key benefit: generic functions adapt to the types you pass in, and carry that information through the return type.
Now that we’ve seen how generics preserve type information, let’s push it further. What if we want to guarantee that the input includes a specific property, like id
?
Generic constraints with extends
Most of the time, we don’t just want a generic to be any type—we want it to be a type we can actually do something with. If we’re calling a method, accessing a property, or relying on structure, we need guarantees.
That’s where generic constraints come in. We use extends
inside the type parameter list to restrict the kinds of types that are allowed. Think of it like this: we’re not saying “accept any type,” we’re saying “accept any type that matches this shape.”
Let’s say we want to log the id
of an object:
function logId<T extends { id: string }>(item: T): void {console.log("ID:", item.id);}logId({ id: "user_123" }); // ✅logId({ id: "post_456", title: "Hello" }); // ✅// logId({ name: "no-id" }); // ❌ Error: Property 'id' is missing
Explanation:
Line 1:
T extends { id: string }
ensures ...