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 generics | With generics |
|---|---|
function first(arr: any[]): any | function first<T>(arr: T[]): T | undefined |
| Return type is lost | Caller gets exact element type |
| Refactor breaks silently | Compiler 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 | undefinedTypeScript 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 numberInterview: 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 missingkeyof 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 constraintInterview:
extendson 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
Tflows from the call site intodata— no manual cast at the usage site. ValidateTat 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 withextendsand 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 shapeBest 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