3. Generics
Generics let a type or function work with many types while preserving the relationship between them. Interviewers care that you can write one, not just consume one.
1. The Problem Generics Solve
// ❌ Loses the type
function firstAny(arr: any[]): any {
return arr[0];
}
const x = firstAny([1, 2, 3]); // any — no autocomplete, no safety
// ✅ Preserves it
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const y = first([1, 2, 3]); // number | undefined
const z = first(["a", "b"]); // string | undefinedT is a type parameter — a placeholder filled in at the call site.
Interview Point
The one-liner: "Generics preserve the relationship between input and output types. any throws that relationship away."
2. Basic Syntax
// Function
function identity<T>(value: T): T {
return value;
}
// Arrow function
const identity = <T,>(value: T): T => value; // trailing comma needed in .tsx
// Interface
interface Box<T> {
value: T;
}
// Type alias
type Pair<A, B> = { first: A; second: B };
// Class
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items.at(-1); }
}
const stack = new Stack<number>();Inference vs Explicit
identity("hello"); // T inferred as string
identity<string>("hi"); // explicit — only needed when inference failsLet TypeScript infer wherever it can.
3. Constraints — extends
Restrict what a type parameter can be.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("hello", "hi"); // OK — strings have length
longest([1, 2], [1, 2, 3]); // OK — arrays have length
// longest(10, 20); // Error — numbers don'tWithout the constraint you cannot access .length at all, because T could be anything.
Multiple Type Parameters
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
const result = merge({ name: "Dipak" }, { age: 25 });
// { name: string; age: number }4. keyof and Indexed Access
The pair that makes most real-world generics work.
interface User {
id: number;
name: string;
email: string;
}
type UserKey = keyof User; // "id" | "name" | "email"
type NameType = User["name"]; // string
type Values = User[keyof User]; // number | stringThe Classic Type-Safe Getter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: "Dipak", email: "d@x.com" };
getProperty(user, "name"); // string
getProperty(user, "id"); // number
// getProperty(user, "age"); // Error — "age" is not a key of UserNote the return type: T[K], not any. The exact property type flows through. This is the single most-asked "write a generic" question.
Type-Safe Setter and Pick
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
pluck(users, "email"); // string[]5. Default Type Parameters
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
const a: ApiResponse = { data: "anything", status: 200 };
const b: ApiResponse<User[]> = { data: users, status: 200 };6. Generic Constraints With Defaults
Real-world example — a typed fetch wrapper:
interface FetchOptions extends RequestInit {
timeout?: number;
}
async function apiClient<TResponse, TBody = unknown>(
url: string,
body?: TBody,
options: FetchOptions = {}
): Promise<TResponse> {
const res = await fetch(url, {
...options,
method: body ? "POST" : "GET",
headers: { "Content-Type": "application/json", ...options.headers },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<TResponse>;
}
const user = await apiClient<User>("/api/users/1");
const created = await apiClient<User, CreateUserDto>("/api/users", dto);Caveat worth stating: res.json() as Promise<TResponse> is an assertion, not a guarantee. In production, parse with Zod — see Narrowing.
7. Generic React Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map((i) => <li key={keyExtractor(i)}>{renderItem(i)}</li>)}</ul>;
}
<List
items={users}
keyExtractor={(u) => u.id}
renderItem={(u) => u.name} // u is inferred as User
/>Full inference from items — no explicit type argument needed at the call site.
The .tsx Arrow Function Gotcha
const f = <T>(x: T) => x; // Error in .tsx — parsed as a JSX tag
const f = <T,>(x: T) => x; // OK — trailing comma disambiguates
const f = <T extends unknown>(x: T) => x; // also works8. Generic Custom Hooks
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = useCallback(
(v: T | ((prev: T) => T)) => {
setValue((prev) => {
const next = v instanceof Function ? v(prev) : v;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
});
},
[key]
);
return [value, setStoredValue] as const; // as const keeps it a tuple
}
const [theme, setTheme] = useLocalStorage("theme", "dark");
// theme: string, setTheme accepts string or (prev: string) => stringas const is the key detail. Without it the return type widens to (string | ((v) => void))[] and destructuring loses the types.
9. Conditional Types
The gateway to advanced TypeScript. Know the syntax; you rarely need to write complex ones.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // falseinfer — Extract a Type
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type B = Unwrap<Promise<User>>; // UserThis is exactly how the built-in ReturnType<T> works:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;Distributive Conditional Types
Conditional types distribute over unions automatically:
type NonNullableT<T> = T extends null | undefined ? never : T;
type A = NonNullableT<string | null>; // stringWrap in a tuple to prevent distribution: [T] extends [U] ? ….
Interview Point
Understanding conditional types matters more for reading library code than for writing app code. If asked, show you can read ReturnType and explain infer — that is enough for an application-developer role.
10. Mapped Types
Transform every property of a type.
type Optional<T> = { [K in keyof T]?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Immutable<T> = { readonly [K in keyof T]: T[K] };Modifiers — Add and Remove
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // remove readonly
type Required<T> = { [K in keyof T]-?: T[K] }; // remove optionalThe - prefix strips a modifier; a bare modifier adds it.
Key Remapping with as
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }Combines mapped types with template literal types. Nice to recognise; rarely needed in app code.
A Genuinely Useful One
// Make some keys optional, keep the rest required
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type NewUser = PartialBy<User, "id" | "createdAt">;
// id and createdAt optional, everything else requiredThis one appears in real codebases constantly — the "create" DTO for an entity.
11. When NOT To Use Generics
A generic used once, with one type parameter appearing in only one position, is just an annotation with extra steps.
// ❌ Pointless — T appears only in the parameter
function log<T>(value: T): void {
console.log(value);
}
// Same as: function log(value: unknown): void
// ✅ Meaningful — T links input to output
function first<T>(arr: T[]): T | undefined {
return arr[0];
}The Rule
A type parameter must appear in at least two places to be doing work — linking a parameter to the return type, or two parameters to each other. Otherwise use unknown.
Saying this unprompted signals you have moved past "generics are cool" into "generics have a purpose".