Deep-dive into TypeScript generics — type parameters, constraints, default types, generic functions, interfaces, and classes. Understand how generics enable type-safe reusability and how to answer every interviewer question from basic syntax to advanced inference.
Generics are type-level parameters — the same concept as function parameters, but for types instead of values. Just as a function like Math.max(a, b) works on any numbers you pass in, a generic like Array<T> works on any element type you supply. The angle-bracket T is a placeholder that gets filled in by the caller, letting you write one implementation that stays type-safe for every concrete type it's used with. Without generics, you'd need to write a separate ArrayOfStrings, ArrayOfNumbers, ArrayOfUsers — generics collapse that explosion into a single, flexible, fully-typed declaration.
Consider a simple identity function. Without generics you must choose between losing type information (using any) or duplicating code (one function per type):
// Bad: any destroys type safety
function identity(arg: any): any {
return arg;
}
const result = identity("hello"); // result is 'any', not 'string'
// Bad: duplication
function identityString(arg: string): string { return arg; }
function identityNumber(arg: number): number { return arg; }
// Good: one generic function, full type safety
function identity<T>(arg: T): T {
return arg;
}
const s = identity("hello"); // s is 'string'
const n = identity(42); // n is 'number'
const u = identity({ id: 1 }); // u is '{ id: number }'
Unconstrained generics give you no information about what T supports. Constraints narrow what types are acceptable and unlock properties on T:
// Without constraint — cannot access .length, T could be anything
function logLength<T>(arg: T): T {
console.log(arg.length); // Error: Property 'length' does not exist on type 'T'
return arg;
}
// With constraint — T must have a length property
function logLength<T extends { length: number }>(arg: T): T {
console.log(arg.length); // OK
return arg;
}
logLength("hello"); // OK — strings have length
logLength([1, 2, 3]); // OK — arrays have length
logLength({ length: 10 }); // OK — object with length property
logLength(42); // Error — numbers have no length
One of the most useful generic patterns: ensuring a key argument actually exists on the object type:
// K must be a key of T — prevents typos and missing-property bugs at compile time
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", email: "alice@example.com" };
const name = getProperty(user, "name"); // type is string
const id = getProperty(user, "id"); // type is number
getProperty(user, "phone"); // Error: Argument of type '"phone"' is not assignable
// to parameter of type '"id" | "name" | "email"'
// Generic interface — describes a typed API response
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: Date;
}
// Usage — T is filled in at the call site
type UserResponse = ApiResponse<User>;
type ProductsResponse = ApiResponse<Product[]>;
// Generic type alias — a reusable Result type (Rust-style)
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// Narrowing works naturally with discriminated unions
function handleResult(r: Result<User>) {
if (r.ok) {
console.log(r.value.name); // r.value is User
} else {
console.error(r.error.message); // r.error is Error
}
}
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[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push("three"); // Error — string not assignable to number
const strStack = new Stack<string>();
strStack.push("hello"); // OK
TypeScript 2.3+ supports default types for generics, making them optional at the call site:
// E defaults to Error if not supplied
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
// Both are valid
type StringResult = Result<string>; // E = Error
type CustomResult = Result<string, string>; // E = string
// Generic with default in function
function createPair<A, B = A>(first: A, second?: B): [A, B | undefined] {
return [first, second];
}
// TypeScript infers T and U from the arguments — no need to annotate at call site
function zip<T, U>(a: T[], b: U[]): [T, U][] {
return a.map((item, i) => [item, b[i]]);
}
const pairs = zip([1, 2, 3], ["a", "b", "c"]);
// pairs is inferred as [number, string][] — no annotation needed
// Curried generic for partial application
const makeTransform = <T>() => <U>(fn: (input: T) => U) => fn;
const stringTransform = makeTransform<string>();
const toNumber = stringTransform(s => parseInt(s, 10)); // (s: string) => numberMany developers think generics are just 'TypeScript's version of any' — the opposite is true. any discards type information entirely. Generics preserve and thread type information through a function or structure so the caller retains full type safety.
Many developers think you must always annotate the type parameter when calling a generic function — TypeScript's type inference usually figures out T from the argument. identity('hello') infers T = string automatically; you only need explicit annotation when inference fails.
Many developers think generic constraints (T extends Something) mean T IS Something — T is constrained to be assignable to Something, not exactly Something. T extends { length: number } means T must have at least a length property; it can have many other properties too.
Many developers think generic classes lock a type at class definition time — the type is locked per instance. new Stack<number>() creates a stack of numbers; new Stack<string>() creates a separate stack of strings. Both come from the same class definition.
Many developers think you need a separate overload for every return type — generics collapse many overloads into one type-safe signature. Instead of three overloads for string, number, and boolean, one generic captures all three.
Many developers avoid generics because they look complex — the most useful patterns (identity, keyof constraint, ApiResponse<T>) are short, predictable, and covered in TypeScript's standard library (Array<T>, Promise<T>, Record<K, V>).
API client wrappers: production codebases define fetchJson<T>(url: string): Promise<T> so every endpoint call is fully typed without casting — the response shape is encoded in the call site, not inside the utility.
React useState hook: useState<User | null>(null) is generics in action — the state is typed as User | null even though useState itself is a generic function in React's type definitions.
Repository pattern: data access layers define Repository<T> with methods like findById(id: string): Promise<T | null> — one class services every entity type while preserving type safety.
Form libraries: React Hook Form and Formik use generics extensively — useForm<LoginFormValues>() types every field, error, and submission handler automatically from a single type argument.
Utility functions: a deeply typed pick(obj, keys) function uses generics and keyof constraints to return only the selected properties with their correct types — impossible to express safely without generics.
Event emitters: typed EventEmitter<{ click: MouseEvent; keydown: KeyboardEvent }> uses generics to ensure on('click', handler) always infers handler's argument as MouseEvent, not the generic Event.
Missing keyof constraint — unsafe property access
Generic identity — T resolves to the argument type
Missing extends object constraint — primitive passed to object utility
Generic function returns any instead of constrained type
What are generics and why are they useful in TypeScript?
What are generic constraints and how do you use the extends keyword with them?
What are default type parameters in TypeScript generics?
What is conditional generic typing — how do you write T extends U ? X : Y?
How do generic functions differ from generic types? Show examples of each.
What is the Partial, Required, and Readonly pattern when building generic utility functions?
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.