Every major TypeScript library—React's useState<T>, Angular's HttpClient.get<T>, RxJS's Observable<T>—relies on generics. Yet when asked "Why use generics instead of any?", most developers freeze. They know generics are "better" but can't articulate why.
The answer is simple: any disables TypeScript's compiler entirely for that code path, while generics preserve type information through transformations. Understanding this distinction—and when to use constraints, conditional types, and the infer keyword—is what TypeScript interviews actually test.
What Are Generics, Really?
Let's start with the question you'll almost certainly hear: "What are generics in TypeScript?"
Here's your 30-second answer:
"Generics let you write reusable code that works with multiple types while keeping type safety. Instead of using
any, you define a type variable likeTthat captures the actual type being used. This way, a function likeidentity<T>(arg: T): Treturns the same type it receives - if you pass a string, TypeScript knows it returns a string."
That's your elevator pitch. Wait for follow-up questions rather than dumping everything you know.
If they want you to go deeper, here's how I'd expand:
"The problem generics solve is a fundamental trade-off in programming: reusability versus type safety. Without generics, you have two bad options. You could write separate functions for each type -
identityString,identityNumber,identityUser- which is tedious and doesn't scale. Or you could useanyand lose all the benefits TypeScript provides.Generics give you a third option. You define a type parameter that acts like a variable, but for types instead of values. When someone calls your function, TypeScript either infers what
Tis based on the arguments, or the caller explicitly specifies it. That type then flows through everywhere you usedTin the function.The real power comes when you combine generics with constraints using
extends. You can say 'T must have a length property' or 'T must be an object with an id field.' This lets you use properties of T while still being generic.Pretty much every serious TypeScript codebase uses generics heavily. React's
useState<T>, API response wrappers, form libraries, state management - they all rely on generics to provide good type safety without sacrificing flexibility."
The any Problem: Why Generics Exist
To really understand generics, you need to feel the pain they solve. Let me show you a scenario I've seen in countless codebases.
Imagine you're writing a simple utility function that returns the first element of an array. Here's the naive approach:
function firstElement(arr: any[]): any {
return arr[0];
}
const numbers = [1, 2, 3];
const first = firstElement(numbers);
// What's the type of 'first'?
// It's 'any' - TypeScript has no idea it's a number
first.toUpperCase(); // No error! But this will crash at runtimeThis code compiles without errors, but it's a bug waiting to happen. TypeScript can't help you because you've told it "I don't care about types here." The connection between what goes in and what comes out is completely lost.
Now let's see the generic version:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const first = firstElement(numbers);
// What's the type of 'first'?
// It's 'number | undefined' - TypeScript knows exactly what it is
first.toUpperCase(); // Error! Property 'toUpperCase' does not exist on type 'number'The <T> declares a type parameter. When we call firstElement(numbers), TypeScript looks at numbers (which is number[]), figures out that T must be number, and knows the return type is number | undefined. The type information flows through.
This is the core insight: generics preserve the relationship between inputs and outputs. With any, that relationship is severed. With generics, TypeScript can follow the types through your code and catch errors before they hit production.
The Identity Function: Your Interview Whiteboard Example
Almost every generics explanation uses the identity function, and there's a reason - it's the simplest possible example that still demonstrates the concept. When an interviewer asks you to demonstrate generics, this is what you write:
function identity<T>(arg: T): T {
return arg;
}Let me break down what's happening here, because understanding this deeply will help you explain more complex examples.
The <T> after the function name declares a type parameter named T. You can think of it like a function parameter, but instead of receiving a value, it receives a type. The name T is just a convention - you could call it Type or Element or anything else, though single capital letters are traditional.
When you write arg: T, you're saying "the argument must be of whatever type T turns out to be." And when you write : T for the return type, you're saying "this function returns the same type it received."
Here's how it works in practice:
const str = identity("hello"); // TypeScript infers T = string, so str: string
const num = identity(42); // TypeScript infers T = number, so num: number
const user = identity({ name: "John", age: 30 }); // T = { name: string; age: number }
// You can also explicitly specify the type
const explicit = identity<string>("hello"); // Sometimes useful for clarityThe magic is that TypeScript does the type inference for you. You don't have to tell it "T is string" - it figures that out from the argument you passed. But the type information isn't lost like it would be with any.
Generic Constraints: When T Isn't Enough
Here's where interviews get interesting. A common follow-up question is: "Write a function that returns the length of any object that has a length property."
If you try the naive approach, TypeScript will complain:
function getLength<T>(arg: T): number {
return arg.length; // Error: Property 'length' does not exist on type 'T'
}TypeScript is being helpful here. It's saying "You've told me T could be anything. A number doesn't have a length property, so I can't let you access .length on something that might be a number."
This is where generic constraints come in. You can tell TypeScript "T can't be just anything - it must be something that has a length property":
function getLength<T extends { length: number }>(arg: T): number {
return arg.length; // Now TypeScript knows arg definitely has .length
}
// These work:
getLength("hello"); // strings have length
getLength([1, 2, 3]); // arrays have length
getLength({ length: 10 }); // objects with length property work too
// This fails at compile time:
getLength(42); // Error: number doesn't have lengthThe extends keyword in T extends { length: number } creates a constraint. It's saying "T must be assignable to an object with a length property." This doesn't mean T is that type - it means T must at least have that property.
A pattern I find more readable is to name the constraint:
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): number {
return arg.length;
}This makes the constraint self-documenting. When someone reads the function signature, they can immediately see what HasLength requires by looking at the interface.
Multiple Type Parameters: When One T Isn't Enough
Sometimes you need more than one type parameter. A classic example is a map function that transforms an array from one type to another:
function map<T, U>(array: T[], transform: (item: T) => U): U[] {
return array.map(transform);
}Here we have two type parameters: T is what we start with, and U is what we end up with. Let's trace through a real example:
const numbers = [1, 2, 3];
const strings = map(numbers, n => n.toString());When TypeScript sees this call, it figures out: numbers is number[], so T = number. The transform function returns a string, so U = string. Therefore the result is string[].
Another practical example is a pair function:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("hello", 42); // Type: [string, number]The convention for naming type parameters is that T stands for "Type," U is the next letter (for a second type), K often means "Key," V means "Value," and E means "Element." These are just conventions, but following them makes your code more readable to other TypeScript developers.
Generic Interfaces and Classes
Generics aren't limited to functions. You can create generic interfaces and classes too, and this is where they really shine in real-world applications.
Consider an API response wrapper. Every endpoint returns data in the same structure, but the actual data differs:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: Date;
}Now you can type all your API responses precisely:
interface User {
id: number;
name: string;
email: string;
}
interface Product {
sku: string;
name: string;
price: number;
}
type UserResponse = ApiResponse<User>;
type ProductListResponse = ApiResponse<Product[]>;
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// When you call this function, TypeScript knows exactly what data contains
const response = await fetchUser(1);
console.log(response.data.name); // TypeScript knows this is a string
console.log(response.data.email); // And this tooGeneric classes follow the same pattern. Here's a simple state container:
class Store<T> {
private state: T;
private listeners: ((state: T) => void)[] = [];
constructor(initialState: T) {
this.state = initialState;
}
getState(): T {
return this.state;
}
setState(newState: Partial<T>): void {
this.state = { ...this.state, ...newState };
this.listeners.forEach(listener => listener(this.state));
}
subscribe(listener: (state: T) => void): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}This Store class works with any state shape, but maintains complete type safety:
interface AppState {
user: User | null;
theme: 'light' | 'dark';
notifications: string[];
}
const store = new Store<AppState>({
user: null,
theme: 'light',
notifications: []
});
// TypeScript ensures you only update with valid properties
store.setState({ theme: 'dark' }); // OK
store.setState({ theme: 'blue' }); // Error: 'blue' is not assignable to 'light' | 'dark'
store.setState({ invalid: true }); // Error: 'invalid' doesn't exist in AppStateUtility Types: Generics in the Wild
TypeScript ships with a collection of built-in generic types called utility types. Understanding these is important for two reasons: they're incredibly useful in practice, and interviewers often ask about them to gauge your TypeScript depth.
Let me walk through the most important ones with practical examples.
Partial and Required
Partial<T> makes all properties of T optional. This is perfect for update functions where you only want to change some fields:
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
// Without Partial, you'd need to pass all properties
function updateUser(id: number, updates: Partial<User>): Promise<User> {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates)
}).then(r => r.json());
}
// Now you can update just what you need
await updateUser(1, { name: 'New Name' }); // Only updating name
await updateUser(1, { email: 'new@email.com', avatar: '/new.png' }); // Multiple fieldsRequired<T> does the opposite - it makes all properties required. This is useful when you have a type with optional properties but need to ensure everything is filled in at a certain point:
interface Config {
host?: string;
port?: number;
ssl?: boolean;
}
function startServer(config: Required<Config>): void {
// Here we're guaranteed all properties exist
console.log(`Starting server at ${config.host}:${config.port}`);
}Pick and Omit
Pick<T, K> creates a type with only the specified properties. Omit<T, K> creates a type without the specified properties. These are complementary tools for shaping types:
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// For API responses, we don't want to send the password
type PublicUser = Omit<User, 'password'>;
// Result: { id: number; name: string; email: string; createdAt: Date; }
// For a login form, we only need these fields
type LoginCredentials = Pick<User, 'email' | 'password'>;
// Result: { email: string; password: string; }
// For creating a new user, we don't want id or createdAt (server generates those)
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;
// Result: { name: string; email: string; password: string; }Record
Record<K, T> creates an object type where all keys of type K map to values of type T. This is incredibly useful for dictionaries and lookup tables:
type Role = 'admin' | 'editor' | 'viewer';
// Permissions for each role
const permissions: Record<Role, string[]> = {
admin: ['read', 'write', 'delete', 'manage-users'],
editor: ['read', 'write'],
viewer: ['read']
};
// User counts by status
type Status = 'active' | 'inactive' | 'pending';
const userCounts: Record<Status, number> = {
active: 150,
inactive: 30,
pending: 12
};ReturnType and Parameters
These utility types extract information from function types, which is powerful for building on top of existing functions:
function createUser(name: string, email: string, role: Role): User {
return {
id: Date.now(),
name,
email,
password: '',
createdAt: new Date()
};
}
// Extract the return type
type CreatedUser = ReturnType<typeof createUser>;
// Result: User
// Extract the parameter types as a tuple
type CreateUserParams = Parameters<typeof createUser>;
// Result: [name: string, email: string, role: Role]
// This is useful for wrapping functions
function withLogging<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return (...args) => {
console.log('Calling function with:', args);
const result = fn(...args);
console.log('Function returned:', result);
return result;
};
}
const createUserWithLogging = withLogging(createUser);
// Fully typed! TypeScript knows the parameters and return typeConditional Types: The Advanced Territory
When interviewers want to test your TypeScript depth, they often bring up conditional types. These are types that choose between two options based on a condition, similar to a ternary operator but for types.
The syntax is: T extends U ? X : Y. If T is assignable to U, the type is X; otherwise, it's Y.
Let's start with a simple example:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true - string literals extend stringThis might seem abstract, but conditional types become powerful when combined with other features. Here's a practical use - a type that unwraps a Promise:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<string>; // string (not a Promise, so unchanged)The infer keyword is doing something special here. It's saying "if T is a Promise, figure out what's inside it and call that U." TypeScript infers U from the structure of T.
Here's another example that extracts the element type from an array:
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<(string | number)[]>; // string | numberDistributive Conditional Types
There's a subtle but important behavior: when you use a conditional type with a union, it distributes over the union members. Each member is evaluated separately, and the results are combined:
type ToArray<T> = T extends unknown ? T[] : never;
// With a single type:
type A = ToArray<string>; // string[]
// With a union - it distributes!
type B = ToArray<string | number>;
// Becomes: (string extends unknown ? string[] : never) | (number extends unknown ? number[] : never)
// Result: string[] | number[]This distribution is what powers utility types like Exclude and Extract:
// Exclude removes union members that match U
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
// How it works:
// 'a' extends 'a' ? never : 'a' → never
// 'b' extends 'a' ? never : 'b' → 'b'
// 'c' extends 'a' ? never : 'c' → 'c'
// never | 'b' | 'c' = 'b' | 'c'
// Extract keeps only union members that match U
type Extract<T, U> = T extends U ? T : never;
type B = Extract<string | number | boolean, number | boolean>; // number | booleanReal-World Patterns
Let me share some patterns I've used repeatedly in production TypeScript code.
Generic React Components
If you work with React and TypeScript, you'll write generic components constantly. Here's a reusable List component:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
if (items.length === 0) {
return <div className="empty">{emptyMessage ?? 'No items'}</div>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}When you use this component, TypeScript infers T from the items you pass:
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [/* ... */];
// TypeScript knows item is User in the callbacks
<List
items={users}
renderItem={(user) => <span>{user.name} ({user.email})</span>}
keyExtractor={(user) => user.id}
/>Generic HTTP Client
Here's a pattern I use in every project - a typed HTTP client:
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async post<TResponse, TBody = unknown>(
endpoint: string,
body: TBody
): Promise<TResponse> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
}
// Usage
const api = new ApiClient('https://api.example.com');
// TypeScript knows user is User
const user = await api.get<User>('/users/1');
// TypeScript checks that the body matches CreateUserDto
// and knows newUser is User
const newUser = await api.post<User, CreateUserDto>('/users', {
name: 'John',
email: 'john@example.com'
});Type-Safe Event Emitter
Here's a generic event emitter that ensures you can only emit events with the correct payload types:
type EventMap = Record<string, any>;
class TypedEventEmitter<T extends EventMap> {
private listeners: { [K in keyof T]?: ((data: T[K]) => void)[] } = {};
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners[event]?.forEach(listener => listener(data));
}
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
if (!this.listeners[event]) return;
this.listeners[event] = this.listeners[event]!.filter(l => l !== listener);
}
}When you use this, TypeScript enforces that events and their data match:
interface AppEvents {
userLoggedIn: { userId: string; timestamp: Date };
userLoggedOut: { userId: string };
error: { message: string; code: number };
}
const emitter = new TypedEventEmitter<AppEvents>();
// TypeScript knows the data shape for each event
emitter.on('userLoggedIn', (data) => {
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
// This is type-safe - wrong data shape would error
emitter.emit('userLoggedIn', { userId: '123', timestamp: new Date() });
// Error: 'timestamp' is missing
emitter.emit('userLoggedIn', { userId: '123' });
// Error: 'unknownEvent' doesn't exist in AppEvents
emitter.emit('unknownEvent', {});Common Interview Follow-Ups
"When would you use unknown vs a generic?"
This is a great question that tests whether you understand the semantic difference:
"
unknownand generics serve different purposes. Useunknownwhen you genuinely don't know or care about the type and won't use it in a type-specific way. Use generics when you need to preserve and flow type information through your code.For example, a logging function might take
unknownbecause it just converts everything to a string - it doesn't matter what the type is. But a data transformation function needs generics because the output type depends on the input type."
// unknown is fine here - we just stringify whatever we get
function log(value: unknown): void {
console.log(JSON.stringify(value));
}
// Generics needed here - return type depends on input
function transform<T, U>(value: T, transformer: (v: T) => U): U {
return transformer(value);
}"What's the difference between extends in constraints vs conditional types?"
"The
extendskeyword means 'is assignable to' in both contexts, but the usage is different.In a constraint like
T extends string, it limits what types can be used as T - only string or its subtypes.In a conditional type like
T extends string ? X : Y, it's a condition being checked - if T is assignable to string, use X; otherwise use Y.Same keyword, different contexts - one is a restriction, one is a test."
"How do you handle default type parameters?"
"You can provide defaults for type parameters, similar to default function parameters. This makes the generic optional when the default is appropriate."
// T defaults to unknown if not specified
interface Container<T = unknown> {
value: T;
metadata: Record<string, string>;
}
// Using default
const container: Container = { value: 'anything', metadata: {} };
// Specifying the type
const typedContainer: Container<number> = { value: 42, metadata: {} };"What's the deal with <T,> in arrow functions?"
This is a practical TSX issue that catches people:
"In JSX/TSX files,
<T>alone is ambiguous - the parser might think it's a JSX element. Adding a trailing comma<T,>or writing<T extends unknown>tells TypeScript it's definitely a generic parameter, not JSX."
// In .tsx files:
const identity = <T,>(arg: T): T => arg; // OK - trailing comma
const identity2 = <T extends unknown>(arg: T): T => arg; // Also OK
// const identity3 = <T>(arg: T): T => arg; // Might confuse the JSX parserWhat Interviewers Are Really Looking For
When I interview candidates on generics, I'm not looking for memorized definitions. I want to see practical understanding and the ability to think through type problems.
The candidates who impress me can explain the core concept simply without jargon. They understand that generics preserve type relationships that would be lost with any. They know how to constrain generics when they need certain properties. They're familiar with the common utility types and can explain when to use them. And for senior roles, they can work through conditional types and explain their reasoning.
What I'm really testing is whether you can apply these concepts to solve real problems. Can you design a generic API response wrapper? Can you spot when a function should be generic versus when it shouldn't? Can you debug type errors involving generics?
The best way to demonstrate this is to think out loud during the interview. If I ask you to write a generic function, explain your reasoning as you write. "I'm making this generic because the return type depends on the input... I'm adding a constraint because I need to access this property..."
Quick Reference
Here's a condensed reference to review before your interview:
| Concept | Syntax | When to Use |
|---|---|---|
| Basic generic | <T> | Preserve type through function/class |
| Multiple params | <T, U> | Transform between different types |
| Constraint | T extends X | Require certain properties |
| Default param | <T = string> | Make type parameter optional |
Partial<T> | - | Make all properties optional |
Required<T> | - | Make all properties required |
Pick<T, K> | - | Select specific properties |
Omit<T, K> | - | Remove specific properties |
Record<K, T> | - | Create object type from keys/values |
ReturnType<F> | - | Extract function return type |
| Conditional | T extends U ? X : Y | Choose type based on condition |
| Infer | infer U | Extract type from structure |
Practice Problems
Test yourself before your interview:
1. Write a generic function last that returns the last element of an array.
2. What's wrong with this code, and how would you fix it?
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge(123, "hello");3. Implement a DeepReadonly<T> type that makes all properties and nested properties readonly.
4. What's the type of Result?
type Flatten<T> = T extends (infer U)[] ? U : T;
type Result = Flatten<string[][]>;Take a moment to work through these before checking the answers.
Answers:
- Here's the solution with proper typing:
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
// For a non-empty array guarantee:
function lastRequired<T>(arr: [T, ...T[]]): T {
return arr[arr.length - 1];
}- The code compiles but produces unexpected results. Spreading primitives like
123and"hello"results in an empty object{}because primitives don't have enumerable properties. The fix is to constrain both type parameters to objects:
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
merge(123, "hello"); // Now this errors at compile time- DeepReadonly requires recursion:
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;- The result is
string[].Flattenunwraps one level of array, sostring[][]becomesstring[]. If you wanted to flatten completely, you'd need a recursive type.
Keep Learning
This guide covers the generics topics that come up most often in interviews, but TypeScript's type system goes much deeper. Topics like mapped types, template literal types, and branded types are worth exploring as you advance.
Our complete TypeScript interview prep covers 50+ questions across type narrowing, module systems, strict mode options, and advanced patterns used in production codebases.
Get Full Access to All TypeScript Questions →
Or try our free TypeScript preview to see more questions like this.
Related Articles
If you found this helpful, check out these related guides:
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- NestJS Interview Guide - TypeScript decorators, generics in DI, and enterprise patterns
- TypeScript Type vs Interface - When to use type aliases vs interfaces
- 7 Tricky TypeScript Interview Questions - Advanced type system questions that test deep knowledge
- 7 Advanced Angular Interview Questions - Advanced patterns, performance, and architecture
Written by the EasyInterview team, based on real interview experience from 12+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.
