Generics Basics
Type parameters for functions, components, and data structures — constraints, inference, and the mistakes that produce any-shaped APIs.
- typescript
- generics
- types
Generics let you write a function, type, or class that works over many types while preserving the relationship between inputs and outputs. Without them you either duplicate code or fall back to any / weak types.
Docs: TypeScript Handbook — Generics, keyof / indexed access.
Why generics exist
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
function firstString(arr: string[]): string | undefined {
return arr[0];
}
Same logic, different types. Generic version:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(['a', 'b']); // string | undefined
T is a type parameter — a placeholder filled by inference or explicit annotation.
Inference vs explicit arguments
first<string>(['x']); // explicit
first(['x']); // inferred as string
Prefer inference when it works. Pass explicit type arguments when:
- The compiler can’t infer (empty arrays, complex conditional types).
- You want a wider or branded type than the literal inference.
- Public API clarity in awkward call sites.
const empty = first<number>([]); // T = number, result number | undefined
Constraints (extends)
Sometimes T can’t be anything — you need properties:
function pluckId<T extends { id: string }>(item: T): string {
return item.id;
}
pluckId({ id: '1', name: 'Ada' }); // ok
// pluckId({ name: 'Ada' }); // error
Constraints are contracts on type parameters. Don’t over-constrain — if you only need id, don’t require a full User interface when a structural { id: string } suffices.
keyof and indexed access
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: '1', age: 42 };
const age = getProp(user, 'age'); // number
// getProp(user, 'nope'); // error
This pattern is the foundation of typed selectors, form libraries, and path helpers.
Multiple type parameters
function mapValues<T extends object, U>(
obj: T,
fn: (value: T[keyof T], key: keyof T) => U,
): Record<keyof T, U> {
const out = {} as Record<keyof T, U>;
(Object.keys(obj) as Array<keyof T>).forEach((k) => {
out[k] = fn(obj[k], k);
});
return out;
}
Name parameters meaningfully when several exist (TItem, TError) — single-letter is fine for short helpers.
Generics on types and interfaces
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
type ApiResponse<T> = {
data: T;
updatedAt: string;
};
interface Repository<T, TId extends string | number = string> {
get(id: TId): Promise<T | null>;
save(entity: T): Promise<void>;
}
Default type parameters (E = Error) keep call sites clean while allowing customization.
Generic functions in React (sketch)
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyFn: (item: T) => string;
};
function List<T>({ items, renderItem, keyFn }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyFn(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage — T inferred from items
<List
items={users}
keyFn={(u) => u.id}
renderItem={(u) => u.name}
/>
Arrow components + generics can be awkward in TSX (const List = <T,>(…) needs a trailing comma). function declarations often parse more cleanly.
Common mistakes
1. Fake generics that don’t relate types
// Bad: T never used in a meaningful relationship
function parse<T>(raw: string): T {
return JSON.parse(raw) as T;
}
This is an assertion in a costume. Prefer unknown + validation, or a generic that depends on a schema argument.
2. Over-using T extends any / unconstrained escape
If you need “accept anything, return the same,” use generics; if you need “accept anything, I will narrow,” use unknown.
3. Nested generics unreadable
Split intermediate types:
type UserDto = { id: string; email: string };
type Page<T> = { items: T[]; nextCursor?: string };
type UserPage = Page<UserDto>;
4. Forcing type arguments everywhere
Fight the compiler with better inference (from arguments) before adding noise at every call.
5. Confusing value space and type space
const enumOrArray = …;
type T = typeof enumOrArray; // type from value
Generics operate on types. Runtime arrays don’t automatically become type parameters without as const / typeof patterns.
Defaults vs inference interaction
function createState<T = string>(initial?: T) {
return { value: initial as T };
}
Defaults apply when inference can’t run. Be careful: a default of string may “win” when you expected a broader inference — test empty call sites.
Interview angle
Explain “preserve input/output relationship.” Write first<T> and getProp with keyof. Call out why parse<T>(string): T is unsafe. Mention constraints and React list component generics.
Live coding: implement Promise.all-like typing for a tuple of promises, or a typed pick(obj, keys).
Related on this site
- unknown vs any
- Cascade & Specificity — different domain; both are “rules for which declaration wins” mental models engineers reuse under pressure
Further reading
- TypeScript Handbook: Generics
- TypeScript Handbook: Object Types
- TypeScript Handbook: Conditional Types (next step after basics)
- TypeScript Handbook: Utility Types
Related guides
- Type NarrowingHow TypeScript refines types through control flow — predicates, discriminants, and patterns that keep strict mode honest.
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.