Topics
v5.0.0
On this page · sections

Generics

Generics let you write reusable functions, hooks, and components that preserve type information instead of falling back to any.

Without genericsWith generics
function first(arr: any[]): anyfunction first<T>(arr: T[]): T | undefined
Return type is lostCaller gets exact element type
Refactor breaks silentlyCompiler catches misuse

Basic generic function

Definition

A type parameter (e.g. T) is a placeholder filled in when the function is called.

function first<T>(items: T[]): T | undefined { return items[0]; } const n = first([1, 2, 3]); // number | undefined const s = first(["a", "b"]); // string | undefined

TypeScript infers T from the argument. You can also pass it explicitly:

first<number>([1, 2, 3]);

Multiple parameters share one type variable when inferred from the same source:

function pair<T>(a: T, b: T): [T, T] { return [a, b]; } pair(1, 2); // [number, number] pair(1, "two"); // ❌ string is not assignable to number

Interview: Generics are compile-time only — they erase at runtime. No performance cost in production JavaScript.

Generic constraints

Definition

Restrict what T can be with extends:

function getId<T extends { id: string }>(item: T): string { return item.id; } getId({ id: "u1", name: "Ada" }); // ✅ getId({ name: "Ada" }); // ❌ Property 'id' is missing

keyof constraint

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { const result = {} as Pick<T, K>; for (const key of keys) { result[key] = obj[key]; } return result; } const user = { id: "1", name: "Ada", role: "admin" as const }; const preview = pick(user, ["id", "name"]); // { id: string; name: string }

Constraining to a primitive

function toArray<T extends string | number>(value: T): T[] { return [value]; } toArray("a"); // string[] toArray(42); // number[] toArray(true); // ❌ boolean does not satisfy constraint

Interview: extends on a generic means “T must be assignable to this shape.” It documents required structure and unlocks property access inside the function.

Generic types and interfaces

Definition

Generics work on type aliases, interfaces, and classes:

type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E }; interface Repository<T> { findById(id: string): Promise<T | null>; save(entity: T): Promise<T>; } class Box<T> { constructor(private value: T) {} get(): T { return this.value; } }

Generic React state

const [user, setUser] = useState<User | null>(null); const [ids, setIds] = useState<string[]>([]);

The useState hook is itself generic — T flows from the initial value or explicit annotation.

Generic custom hook: useFetch

Real-world example

A typed data-fetching hook is the classic senior frontend generic:

type FetchState<T> = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: string }; function useFetch<T>(url: string): FetchState<T> { const [state, setState] = useState<FetchState<T>>({ status: "idle" }); useEffect(() => { let cancelled = false; setState({ status: "loading" }); fetch(url) .then((res) => { if (!res.ok) throw new Error(res.statusText); return res.json() as Promise<T>; }) .then((data) => { if (!cancelled) setState({ status: "success", data }); }) .catch((err: unknown) => { if (!cancelled) { setState({ status: "error", error: err instanceof Error ? err.message : "Unknown error", }); } }); return () => { cancelled = true; }; }, [url]); return state; }

Usage:

type User = { id: string; name: string }; function Profile() { const state = useFetch<User>("/api/user"); if (state.status === "success") { return <p>{state.data.name}</p>; // data is User } // ... }

Interview: The generic T flows from the call site into data — no manual cast at the usage site. Validate T at the fetch boundary with a schema.

Generic components

Definition

type ListProps<T> = { items: T[]; renderItem: (item: T) => React.ReactNode; keyExtractor: (item: T) => string; }; function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { return ( <ul> {items.map((item) => ( <li key={keyExtractor(item)}>{renderItem(item)}</li> ))} </ul> ); } // T inferred as { id: number; label: string } <List items={[{ id: 1, label: "One" }]} keyExtractor={(item) => String(item.id)} renderItem={(item) => <span>{item.label}</span>} />

Multiple type parameters

Definition

function mapRecord<K extends string, V, R>( record: Record<K, V>, fn: (value: V, key: K) => R, ): Record<K, R> { const out = {} as Record<K, R>; for (const key in record) { out[key] = fn(record[key], key); } return out; }

Independent type parameters for input/output transformation:

function map<T, U>(items: T[], fn: (item: T) => U): U[] { return items.map(fn); } const lengths = map(["a", "bb"], (s) => s.length); // number[]

Default type parameters

Definition

type ApiResponse<T = unknown> = { data: T; meta: { page: number }; }; type AsyncResult<T = void> = | { status: "pending" } | { status: "resolved"; value: T };

Defaults reduce boilerplate when most call sites use the same type:

const response: ApiResponse = { data: { any: "shape" }, meta: { page: 1 } }; const typed: ApiResponse<User[]> = { data: users, meta: { page: 1 } };

React context pattern:

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined); function useTheme(): ThemeContextValue { const ctx = useContext(ThemeContext); if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); return ctx; }

Interview: Add generics when reuse is real and inference carries meaningful type information. Do not genericize one-off helpers.

Senior Interview Summary

What to say

Generics parameterize types so one implementation works for many shapes while preserving inference. In React, they are essential for hooks (useState<T>), list components, and fetch wrappers. Constrain with extends and validate at API boundaries.

Common pitfalls

Over-genericizing simple functions → use concrete types when T adds no value Using any instead of T → loses inference at call site Forgetting constraints → T extends ... documents required shape Empty array inference → annotate or cast the initial value Runtime validation skipped → generic T does not validate JSON shape

Best mental model

<T> → "I will learn the type from how you call me" extends → "T must at least look like this" default → "use this type when caller does not specify"

Practical rule

Reusable hook/component → generic One-off function → concrete types unless reuse is real API response → generic + runtime validation (Zod) at boundary Literal preservation → T extends string or as const at call site

Last updated on