A plain generic function called with ["a", "b"] infers T as the widened `string` — but mark the type parameter <const T> (stable since TypeScript 5.0) and the exact same call infers the literal tuple instead, confirmed by compiling both side by side. This flagship topic covers generics from first principles through that specific, often-missed modern capability.
1. What's the fundamental difference between a generic type parameter <T> and `any`?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
function firstElement<T>(arr: T[]): T { return arr[0]; }
→ T is a PLACEHOLDER, resolved per call — NOT the same as `any`
(any DISABLES checking; T stays fully checked, just parameterized)
CONSTRAINTS: <T extends { length: number }>
→ bounds what T can be; enables safe access to guaranteed properties
→ only add a constraint the function body ACTUALLY needs
INFERENCE (default) vs EXPLICIT type arguments:
wrap("hello") → T inferred as string
wrap<string | number>("hello") → T explicitly WIDENED, overrides inference
(explicit args still must be ASSIGNABLE to what's specified)
<const T> — stable since 5.0:
function tuple<const T extends readonly unknown[]>(...args: T): T
tuple("a", 1, true)
WITHOUT const → T: (string|number|boolean)[] (WIDENED)
WITH const → T: readonly ["a", 1, true] (LITERAL preserved)
⚠️ affects TYPE INFERENCE only — does NOT make nested objects
runtime-immutable.
Pre-5.0 workaround: callers manually add `as const` everywhere.
GENERIC COMPONENTS: same mechanism, applied to React —
function List<T>({ items, renderItem }: ListProps<T>) { ... }
(T inferred from the `items` prop — see TypeScript with React)
Generics = FULLY erased at compile time — ZERO runtime cost.
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: numberconst str = firstElement(["a", "b"]); // T = string, str: string
T 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.
Generic type parameters: inferred from arguments, then flow through the return type
step 1 / 4
function first<T>(arr: T[]): T {
return arr[0];
}
What the compiler knows here
Tunchanged
unresolved
not yet called — T is just a placeholder
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 .lengthgetLength([1, 2, 3]); // ✅ arrays have .lengthgetLength(
extends 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.
function wrap<T>(value: T): T[] { return [value];}const inferred = wrap("hello"); // T inferred as string, from the argumentconst explicit = wrap<string | number>("hello"); // T EXPLICITLY specified — overrides inference
Most 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:
Confirmed 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.
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.
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.
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 =
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.
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.
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 numbers
Only 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.
Prefer a generic over any whenever a function's return type genuinely depends on its input type — generics preserve that relationship; any discards 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.
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.
42
);
// ❌ error — number doesn't have .length
readonly
[
"a"
,
1
,
true
]
=
a;
// ✅ compiles — the EXACT literal tuple was preserved
>
{
items.
map
((
item
,
i
)
=>
<
li
key
=
{
i
}
>
{
renderItem
(item)
}
</
li
>)
}
</
ul
>;
}
<List items={[1, 2, 3]} renderItem={(n) => <span>{n * 2}</span>} /> // T inferred as number
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`