Generics are the mechanism TypeScript uses to let you write code that is both reusable across types and still fully type-safe. Without generics, you either repeat yourself (one function per type) or lose type information (use any). With generics, you write the function once and let the type system track the specific type through every call.
What changed in 2026
- TypeScript 5.x brought const type parameters and improved inference.
const generics let you capture literal types rather than widened types, reducing the need for as const casts in many patterns.
satisfies + generics patterns matured. The combination of satisfies (TS 4.9) with generic constraints gives you validation without losing the literal type — widely used in config and schema libraries.
- Template literal types in generics became practical. Building typed route parameters, event names, and CSS properties using template literal types is now a real production pattern, not just a party trick.
- Variance annotations (
in, out) were added in TS 4.7 and are now part of standard library type definitions, making complex generic hierarchies more predictable.
The basics: a generic function
Without generics:
function first(arr: number[]): number | undefined {
return arr[0];
}
// Only works for number[]. You'd need to duplicate for string[], etc.
With a generic:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(['a', 'b', 'c']); // s: string | undefined
const u = first([]); // u: undefined
TypeScript infers T from the argument — you don't write first<number>([1, 2, 3]) unless inference fails.
Type constraints with extends
// Without constraint — you can't access .id because T might not have it
function getById<T>(items: T[], id: unknown): T | undefined {
return items.find((item) => (item as any).id === id); // unsafe!
}
// With constraint — T must have an id property
function getById<T extends { id: string | number }>(
items: T[],
id: T['id']
): T | undefined {
return items.find((item) => item.id === id);
}
// Now TypeScript knows item.id exists and is string | number
const user = getById(users, 'user_123'); // typed correctly
T extends { id: string | number } is not a runtime check — it's a compile-time constraint that tells TypeScript what properties to expect on T.
Generic interfaces and types
// A typed API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// A typed key-value store
type Store<K extends string, V> = {
get(key: K): V | undefined;
set(key: K, value: V): void;
keys(): K[];
};
// Real usage
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// TypeScript knows response.data is a User
Conditional types and infer
Conditional types let you compute a type based on another:
// Extract the resolved type from a Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<number>; // number (not a Promise, returns as-is)
// Extract function return type
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
// Extract the element type from an array
type ElementType<T extends any[]> =
T extends (infer E)[] ? E : never;
type Elem = ElementType<string[]>; // string
infer inside a conditional type creates a new type variable that captures a part of the matched type. It is the foundation of most utility types in the TypeScript standard library.
Utility types you should know
| Utility type |
What it does |
Partial<T> |
Makes all properties optional |
Required<T> |
Makes all properties required |
Readonly<T> |
Makes all properties readonly |
Pick<T, K> |
Keeps only keys K from T |
Omit<T, K> |
Removes keys K from T |
Record<K, V> |
Object type with keys K and values V |
ReturnType<F> |
Return type of function F |
Parameters<F> |
Parameter tuple of function F |
Awaited<T> |
Unwraps Promise type |
NonNullable<T> |
Removes null and undefined from T |
How to pick
- Does your function work identically for multiple types? Add a generic. If it only ever handles
string, just type it as string.
- Do you need to access properties on the type parameter? Add a constraint (
T extends { ... }). Without it, TypeScript won't know what's available.
- Does a return type depend on an input type in a non-trivial way? Use conditional types or function overloads.
- Is the generic getting hard to read? Name it meaningfully (
TItem, TKey, TResponse) instead of single letters beyond T in simple cases.
- Are you using
any inside a generic? That's a sign the generic isn't doing its job — tighten the constraint or restructure the function.
Common mistakes
Using generics for a function with one concrete type. function parse<T>(json: string): T is not safer than function parse(json: string): unknown — in fact it's less safe, because the caller asserts the type without any check. Use unknown and validate.
Overly loose constraints. T extends object is almost as weak as T extends any for most purposes — it excludes primitives but tells you nothing about shape. Constrain to the actual shape you need.
Forgetting to constrain key generics. function pluck<T, K>(obj: T, key: K) has no constraint relating K to T. Use K extends keyof T to make TypeScript verify the key exists.
Using generics to avoid writing a union type. Sometimes string | number is clearer than T extends string | number. Generics add complexity; only use them when the type needs to flow through to the output.
What to skip
- Deeply nested conditional types in application code. They're powerful in library code but become maintenance nightmares in application logic. If you need more than two levels of conditional types, consider a different approach.
- Generic classes for simple data bags. A generic
Container<T> is sometimes overkill where a typed interface would do. Classes add runtime cost; interfaces are erased.
- Fighting inference with explicit type arguments. TypeScript's inference is good.
first<string>(['a', 'b']) is unnecessary — first(['a', 'b']) infers correctly. Only provide explicit type arguments when inference actually fails.
FAQ
What is the difference between T extends any and T?
Effectively none for constraints. T extends any is a vacuous constraint — everything extends any. Omit it; just write T.
When should I use unknown vs a generic?
Use unknown when you don't know the type and the function doesn't need to return it in a typed way. Use a generic when the type of the output depends on the type of the input — the function "passes through" the type information.
What are variance annotations (in, out)?
in marks a type parameter as contravariant (only used in input positions); out marks it as covariant (only used in output positions). They improve type-checking performance and accuracy for complex generic types. Used primarily in library code, not typical application code.
How do I type a function that accepts any object and returns a deep partial version?
Use a recursive conditional type: type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }. This is a common library utility; many type libraries export it so you don't need to write it yourself.
Where to go next
gRPC vs REST in 2026, GraphQL vs REST in 2026, and API rate limiting in 2026.