Lesson 11 of 25
Generics Fundamentals
Write one function that works across many types without losing type safety, using type parameters, constraints with extends, and default type arguments.
Some functions and types genuinely don't care what type they're working
with — they just need to work with some type consistently. Generics are
how you write that once, instead of duplicating it per type or falling back
to any and losing type safety entirely.
The problem generics solve
Here's a function that returns whatever it was given:
function identity(value: any): any {
return value;
}
const result = identity("hello");
result.toUpperCase(); // compiles — but so would result.push(1), or anything elseany "works" here, but it throws away the one thing this function actually
guarantees: whatever type goes in comes back out, unchanged. A type
parameter captures that relationship instead:
function identity<T>(value: T): T {
return value;
}
const result = identity("hello"); // T inferred as string
result.toUpperCase(); // fine — result is string, not any
const num = identity(42); // T inferred as number
num.toUpperCase();
// Property 'toUpperCase' does not exist on type 'number'.<T> declares a type parameter — a placeholder type, filled in per call.
TypeScript infers T from the argument you actually pass, the same way it
infers a variable's type from its initializer. You rarely need to specify it
explicitly (identity<string>("hello")), though you can when inference
alone wouldn't have enough to go on.
A more realistic example
function first<T>(items: T[]): T | undefined {
return items[0];
}
const firstNumber = first([1, 2, 3]); // T = number, so number | undefined
const firstName = first(["Amara", "Jae"]); // T = string, so string | undefinedOne function definition, correctly typed for every array it's called with —
no duplication, and no any anywhere in sight.
Constraints: T extends
An unconstrained T could be anything, which means you can't assume it has
any properties or methods at all inside the function body:
function logLength<T>(value: T) {
return value.length;
// Property 'length' does not exist on type 'T'.
}A constraint restricts what T is allowed to be, using extends — not
the class-inheritance keyword from earlier, but the same word reused for "T
must be assignable to this type":
function logLength<T extends { length: number }>(value: T) {
return value.length; // fine — every T here is guaranteed to have .length
}
logLength("hello"); // fine — strings have .length
logLength([1, 2, 3]); // fine — arrays have .length
logLength(42);
// Argument of type 'number' is not assignable to
// parameter of type '{ length: number; }'.This is the pattern to reach for instead of any whenever you need "some
type, but it must have at least these properties." It's strictly more
precise than any (which allows literally everything, safe or not) and more
flexible than a single concrete type (which would only work for one shape).
Multiple type parameters
Generics aren't limited to one parameter — a function combining two independent values commonly needs two:
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const result = pair("age", 30); // [string, number]Default type parameters
A type parameter can have a default, used when it can't be inferred and the caller doesn't specify one explicitly:
interface ApiResponse<T = unknown> {
status: number;
data: T;
}
const generic: ApiResponse = { status: 200, data: "anything" }; // T defaults to unknown
const typed: ApiResponse<{ name: string }> = { status: 200, data: { name: "Amara" } };This mirrors default function parameters — it's about ergonomics for the common case, while still allowing a more specific type argument when one is known.
Try it yourself
What to remember
- A type parameter (
<T>) captures a relationship between a function's input and output types, without giving up type safety the wayanywould. - TypeScript infers
Tfrom the arguments at each call site — you rarely need to specify it explicitly. <T extends Shape>constrains what T is allowed to be, so the function body can safely assume properties the constraint guarantees.- A default type parameter (
<T = Default>) is used when T can't be inferred and the caller doesn't supply one.
Check yourself
4 questions · pass 3/4 to unlock Generic Interfaces and Classes
1.What problem does
function identity<T>(value: T): T { return value; }solve thatfunction identity(value: any): any {}doesn't?2.What does
<T extends { length: number }>restrict a generic type parameter to?3.In
function wrap<T = string>(value?: T), what does= stringdo?4.Why is
function first<T>(items: T[]): T { return items[0]; }better than a version typed(items: any[]): any?
4 left to answer