Concept
The beginner framing: a function that works identically regardless of the specific type it's handling, an array-reversing function, a "get the first element" helper, shouldn't have to be rewritten once per type. Generics let a function, interface, or type alias take a type parameter, filled in with a concrete type at each point it's actually used.
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const num = firstElement([1, 2, 3]); // T = number, num: number
const str = firstElement(["a", "b"]); // T = string, str: stringT is a placeholder, resolved to a concrete type each time the function is called, this is fundamentally different from any, which disables type-checking; T still gets full type-checking, just parameterized rather than fixed to one specific type.
function first<T>(arr: T[]): T {return arr[0];}
Before `first` is ever called, `T` is an unresolved placeholder, it only gets a concrete type once the function is actually invoked with real arguments.
Constraints: bounding what a type parameter can be
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // ✅ strings have .length
getLength([1, 2, 3]); // ✅ arrays have .length
getLength(42); // ❌ error, number doesn't have .lengthextends on a type parameter constrains what it's allowed to be, without a constraint, T could be absolutely anything, meaning the function body can't safely assume it has any particular property. T extends { length: number } says "T can be anything, as long as it has a .length property", enabling item.length inside the function body to type-check safely.
Inference vs. explicit type arguments
function wrap<T>(value: T): T[] {
return [value];
}
const inferred = wrap("hello"); // T inferred as string, from the argument
const explicit = wrap<string | number>("hello"); // T EXPLICITLY specified, overrides inferenceMost of the time, T is inferred automatically from the arguments passed, no need to write wrap<string>("hello") manually. An explicit type argument is useful specifically when the desired type is wider than what inference alone would produce from the argument (as shown above), or when there's no argument to infer from at all.
const type parameters, stable since TypeScript 5.0
function tuple<const T extends readonly unknown[]>(...args: T): T {
return args;
}
const a = tuple("a", 1, true);
// WITHOUT const: T would infer as (string | number | boolean)[], WIDENED
// WITH const: T infers as the exact literal tuple: readonly ["a", 1, true]
const check: readonly ["a", 1, true] = a; // ✅ compiles, the EXACT literal tuple was preservedConfirmed by compiling both forms directly: without const, calling a generic function with "a", 1, true infers T as the widened (string | number | boolean)[], the specific literal values and their exact positions are lost. Marking the type parameter <const T extends readonly unknown[]> changes this: T is now inferred using each argument's literal type, preserving the exact tuple readonly ["a", 1, true] rather than widening to a general union array. This directly matters for APIs (like state-machine or routing libraries) that need to know the exact values passed, not just their general types, before TypeScript 5.0, this required callers to manually add as const at every call site; now the function signature itself can guarantee it.
Generic components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
This same generic-parameter mechanism extends naturally to React components, a List component can render items of any type while keeping renderItem's parameter correctly typed to match whatever items actually contains, with T inferred from the items prop at each usage. This gets its own full treatment, with props/hooks/refs specifically, in TypeScript with React.
Try It
Predict the outcome before checking the solution.
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("hello", 42);What's the inferred type of result? What if you called pair<string, string>("hello", 42) instead?
Solution
result is inferred as [string, number], TypeScript infers T and U independently, one from each argument, and the return type [T, U] resolves to the tuple [string, number]. Calling pair<string, string>("hello", 42) would fail to compile: explicitly specifying U as string conflicts with the actual argument 42, which is a number, an explicit type argument overrides inference, but the argument still has to be assignable to that explicitly specified type, and 42 isn't assignable to string.
Implement It Yourself
Build a small generic utility with a constraint, to internalize how extends shapes what's allowed inside the function body:
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada", active: true };
const name = pluck(user, "name"); // inferred: string
const id = pluck(user, "id"); // inferred: number
const bad = pluck(user, "email"); // ❌ error, "email" isn't a key of `user`K extends keyof T constrains K to only the actual property names of T, this is what makes pluck(user, "email") a compile error rather than a silent undefined at runtime, and it's exactly the mechanism behind TypeScript's built-in Pick/Record utility types, covered next in Utility Types.
Under the Hood
Generic type inference is the mechanism underlying much of Utility Types (Partial<T>, Pick<T, K>, and the rest are themselves generic types) and Advanced Types (conditional types frequently combine with generics to express relationships between input and output types). And T extends {...} constraints use the exact same extends keyword and structural-compatibility checking established generally in TypeScript Basics, a constraint is really just "T must be structurally compatible with this shape," applied to a type parameter instead of a concrete value.
Common Mistakes
1. Reaching for any instead of a generic when a function should work across types
function firstElement(arr: any[]): any { // ❌ loses ALL type safety
return arr[0];
}This compiles, but the caller gets zero type information back, firstElement([1,2,3]) returns any, not number. A generic (<T>(arr: T[]): T) preserves the actual relationship between input and output types.
2. Adding an unnecessary constraint that narrows T more than needed
function identity<T extends object>(value: T): T { // ❌ unnecessarily excludes primitives
return value;
}
identity(42); // now an error, even though identity() has no real reason to reject numbersOnly constrain T when the function body genuinely needs to rely on something the constraint guarantees, an unconstrained <T> is more broadly usable when there's no actual structural requirement.
3. Expecting <const T> to make the resulting value deeply immutable at runtime
function tuple<const T extends readonly unknown[]>(...args: T): T { return args; }
const a = tuple({ x: 1 }, "b");
a[0].x = 99; // ✅ still compiles, const type parameters affect TYPE inference, not runtime mutability<const T> changes how TypeScript infers the type (preserving literals), but it doesn't make the underlying object deeply readonly at every level, a tuple element that's itself an object remains mutable unless separately marked readonly.
Best Practices
- Prefer a generic over
anywhenever a function's return type genuinely depends on its input type, generics preserve that relationship;anydiscards it entirely. - Only add a constraint (
extends) when the function body actually needs it, an unconstrained type parameter is more flexible and shouldn't be narrowed "just in case." - Reach for
<const T>specifically when an API needs to know the exact literal values passed, not just their general types (state machines, routing configs, anything keying behavior off specific string/number values). - Let inference do the work, only supply explicit type arguments when inference alone can't produce the type you actually need.
Performance Tips
- Generic type parameters are fully resolved and erased at compile time, a generic function compiles to the exact same JavaScript as its non-generic equivalent would, with zero runtime overhead from the type parameter itself.
- Extremely deep or complex generic constraints can slow down
tsc's type-checking on large codebases, this is purely a build-time cost, never a runtime one, but worth knowing if compile times become a concern with heavily generic code.
