Every major TypeScript library—React's useState<T>, Angular's HttpClient.get<T>, RxJS's Observable<T>—uses generics to express relationships between types. This guide contains exactly 25 questions and reflects stable TypeScript 6.0 plus the TypeScript 7 native preview status documented in September 2026.
The important contrast is not “generics good, any bad.” Generics preserve a relationship, unknown represents a value that must be narrowed, and any opts out of many checks. All are erased or transformed before runtime: a generic return annotation cannot validate JSON, authorize a field, or prevent an invalid value from another system.
Table of Contents
- Generic Fundamentals Questions
- Generic Functions Questions
- Generic Constraints Questions
- Generic Interfaces and Classes Questions
- Utility Types Questions
- Conditional Types Questions
- Real-World Pattern Questions
- Advanced Generics Questions
- Quick Reference
Generic Fundamentals Questions
Understanding the core purpose and mechanics of generics is essential for any TypeScript interview.
What are generics in TypeScript?
Generics let an API relate types while remaining reusable. A type parameter like T is not a runtime value; it is a compile-time placeholder inferred or supplied at a use site. Thus identity<T>(arg: T): T says the output has the same type as the input. It does not say that every implementation with a generic parameter is automatically safe.
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 use any and 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 T is based on the arguments, or the caller explicitly specifies it. That type then flows through everywhere you used T in the function.
Why use generics instead of the any type?
The critical difference is that a useful generic preserves a relationship while any opts out of many checks and can propagate that opt-out into surrounding expressions. unknown is safer for an untrusted value because code must narrow it before type-specific use.
Consider a function that returns the first element of an array. With any, TypeScript has no idea what type is returned:
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 runtimeWith generics, TypeScript preserves the relationship between input and output:
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.
Generic Functions Questions
Writing generic functions is a core skill tested in TypeScript interviews.
How do you write a basic generic function?
The identity function is the simplest example that demonstrates generics. It returns exactly what it receives, and TypeScript preserves the type information:
function identity<T>(arg: T): T {
return arg;
}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."
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 clarityTypeScript 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.
How do you use multiple type parameters?
Sometimes you need more than one type parameter, especially when transforming from one type to another. A classic example is a map function:
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. TypeScript traces through:
const numbers = [1, 2, 3];
const strings = map(numbers, n => n.toString());
// numbers is number[], so T = number
// The transform 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: T stands for "Type," U is the next letter (for a second type), K often means "Key," V means "Value," and E means "Element."
Generic Constraints Questions
Constraints allow you to restrict what types can be used with a generic, enabling you to access specific properties.
What are generic constraints and why do you need them?
Generic constraints limit what types can be used with a generic using the extends keyword. Without constraints, TypeScript assumes T could be anything, so it won't let you access specific properties.
If you try to access a property on an unconstrained generic, TypeScript complains:
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."
Constraints solve this by telling TypeScript "T must have 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 says the type argument must be assignable to a structure with a numeric length property. This is structural and compile-time only: external values still require runtime validation.
How do you create named constraints for better readability?
A pattern that makes code more self-documenting is to name the constraint in an interface:
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): number {
return arg.length;
}When someone reads the function signature, they can immediately see what HasLength requires by looking at the interface. This is especially useful when the constraint is used in multiple places.
How do you constrain one type parameter based on another?
You can constrain one type parameter to be a key of another, which is essential for type-safe property access:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "John", age: 30, email: "john@example.com" };
const name = getProperty(user, "name"); // Type: string
const age = getProperty(user, "age"); // Type: number
getProperty(user, "invalid"); // Error: "invalid" is not a key of userThe K extends keyof T constraint ensures that key must be a valid property name of T. The return type T[K] is an indexed access type that gives the type of that property.
Generic Interfaces and Classes Questions
Generics aren't limited to functions—they're essential for creating reusable data structures and components.
How do you create a generic interface?
Generic interfaces let you define reusable type shapes that work with any type. A common example is an API response wrapper:
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 tooHow do you create a generic class?
Generic classes follow the same pattern as generic interfaces. 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 Questions
TypeScript ships with built-in generic utility types that are frequently asked about in interviews.
What is Partial and how do you use it?
Partial<T> makes the top-level properties of T optional. It can model part of an internal update shape, but it is shallow and is rarely a complete public PATCH contract: immutable/server-owned fields, authorization, “missing” versus explicit undefined, validation, and nested update semantics need deliberate design.
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
type UserPatch = Partial<Pick<User, 'name' | 'email' | 'avatar'>>;
function updateUser(id: number, updates: UserPatch): Promise<Response> {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates)
});
}
// 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 fieldsWhat is Required and when do you use it?
Required<T> removes optional modifiers at the top level. It does not recursively require nested properties, validate a runtime config, or necessarily remove an explicitly declared undefined from a property's union.
interface Config {
host?: string;
port?: number;
ssl?: boolean;
}
function startServer(config: Required<Config>): void {
// Compile-time contract: all top-level keys are required.
console.log(`Starting server at ${config.host}:${config.port}`);
}What is Pick and Omit and how do they differ?
Pick<T, K> creates a type with selected keys. Omit<T, K> removes keys from a type. Both are compile-time transformations: Omit<User, 'password'> does not strip a password property from an object at runtime, so response serialization still needs an explicit allowlist or mapper.
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; }What is Record and when do you use it?
Record<K, T> maps every key in K to T. A finite literal union can enforce an exhaustive table. Record<string, T> is an index-signature-like type and does not prove that any arbitrary key exists at runtime; with unchecked indexed access, reads may need undefined handling.
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
};What is 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]
// Preserve an argument tuple and result relationship without any.
function withLogging<Args extends unknown[], Result>(
fn: (...args: Args) => Result
): (...args: Args) => Result {
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 typeFor an async function, ReturnType is the Promise type. Compose it with Awaited for the fulfilled value. With overloaded function types, these utilities use the type visible to their conditional inference (commonly the last signature), not runtime overload resolution.
Conditional Types Questions
Conditional types are advanced TypeScript that interviewers use to test depth of knowledge.
What are conditional types and how do they work?
Conditional types select one of two types 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.
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 stringConditional types become powerful when combined with other features. A one-layer teaching example can unwrap 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)Production code should usually use Awaited<T> for await-like recursive thenable unwrapping.
How do you use the infer keyword?
The infer keyword is used within conditional types to extract and capture a type. It declares a type variable that TypeScript infers from the structure.
In T extends Promise<infer U>, the infer U is 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 an example that extracts the element type from an array:
type ElementType<T> = T extends readonly (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<(string | number)[]>; // string | numberWhat are distributive conditional types?
When the checked side is a naked type parameter, a conditional type distributes over 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[]Wrap each side in a tuple to suppress distribution:
type ToArrayNonDistributive<T> =
[T] extends [unknown] ? T[] : never;
type C = ToArrayNonDistributive<string | number>;
// (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 Pattern Questions
Interviewers often ask how generics are used in practice.
How do you create a generic React component?
Generic components are essential in React TypeScript. 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}
/>How do you create a generic HTTP client?
A generic HTTP client must not let the caller assert that untrusted JSON is T. Type parameters are erased, and response.json() crosses a runtime trust boundary. Accept a decoder/schema that turns unknown into T or throws:
class ApiClient {
constructor(private baseUrl: string) {}
async get<T>(
endpoint: string,
decode: (input: unknown) => T
): Promise<T> {
const response = await fetch(this.baseUrl + endpoint);
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
const input: unknown = await response.json();
return decode(input);
}
async post<TBody, TResponse>(
endpoint: string,
body: TBody,
decode: (input: unknown) => TResponse
): 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}`);
}
const input: unknown = await response.json();
return decode(input);
}
}
const api = new ApiClient('https://api.example.com');
declare const decodeUser: (input: unknown) => User;
const user = await api.get('/users/1', decodeUser);
const body = {
name: 'John',
email: 'john@example.com',
password: 'secret'
} satisfies CreateUserDto;
const newUser = await api.post('/users', body, decodeUser);The body type checks trusted application code; the server must independently authenticate, authorize, validate, and reject excess or forbidden fields.
How do you create a type-safe event emitter?
A generic event emitter ensures you can only emit events with the correct payload types:
type ErasedListener = (data: unknown) => void;
class TypedEventEmitter<Events extends object> {
private listeners = new Map<keyof Events, Set<ErasedListener>>();
on<K extends keyof Events>(
event: K,
listener: (data: Events[K]) => void
): () => void {
const erased = listener as ErasedListener;
const set = this.listeners.get(event) ?? new Set();
set.add(erased);
this.listeners.set(event, set);
return () => set.delete(erased);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(listener => listener(data));
}
}The implementation erases listener payloads only inside one private storage boundary because TypeScript cannot directly express the correlation between each Map key and its value set. Public on and emit signatures preserve that correlation; runtime callers and external messages still need validation.
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', {});Advanced Generics Questions
These questions test deep understanding of TypeScript's type system.
When would you use unknown vs a generic?
unknown and generics serve different purposes. Use unknown at a boundary where the type is not yet trusted and narrow it before type-specific work. Use a generic when an API must preserve a relationship between two or more type positions.
A logging function might take unknown because 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: console.log accepts it without narrowing.
function log(value: unknown): void {
console.log(value);
}
// Generics needed here - return type depends on input
function transform<T, U>(value: T, transformer: (v: T) => U): U {
return transformer(value);
}What are variance annotations and when should you use them?
Variance describes how assignability between type arguments relates to assignability between generic types. A producer is commonly covariant, while a consumer is commonly contravariant:
interface Producer<out T> {
make(): T;
}
interface Consumer<in T> {
consume: (value: T) => void;
}TypeScript's structural type system normally infers variance. Explicit in/out annotations are an advanced diagnostic or performance tool for specific recursive types, not a way to force arbitrary behavior. They are consulted only in instantiation-based comparisons of the same generic type and must match the type's actual structural use. Add them only after profiling or isolating an inference problem, and validate the annotation carefully.
What is the difference between extends in constraints vs conditional types?
The extends keyword 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 can be passed.
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 provide 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: {} };Why do you need the trailing comma in arrow function generics in TSX?
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 parserQuick Reference
Key generic concepts and syntax
| 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 |
| Variance | in T / out T | Rarely document inferred input/output variance |
Frequently Asked Questions
What are generics in TypeScript?
Generics introduce type parameters so an API can express relationships between input, output, stored values, or callbacks. For example, identity
What is the difference between generics and the any type?
A generic preserves a declared relationship, such as returning the same type that entered. any opts out of many checks and can contaminate surrounding expressions, while unknown accepts an untrusted value but requires narrowing before use. A generic is useful only when its parameter relates multiple positions or constrains behavior; a type parameter used once may add no information.
What are generic constraints in TypeScript?
A constraint such as T extends { length: number } requires an inferred or explicit type argument to be assignable to that structure and lets the implementation use length. K extends keyof T relates a key to an object and T[K] preserves the selected property type. Constraints are compile-time checks; they do not inspect runtime values or guarantee that external data follows the declaration.
What are TypeScript utility types?
Utility types are generic type transformations: Partial and Required change optional modifiers, Readonly changes readonly modifiers, Pick and Omit select property keys, Record maps keys to values, and Parameters or ReturnType inspect function types. Most are shallow and exist only in the type system. Omit does not delete a password at runtime, Partial is not automatic PATCH validation, and Readonly does not freeze an object.
What are conditional types in TypeScript?
A conditional type T extends U ? X : Y chooses a type branch using assignability. When the checked side is a naked type parameter, a union normally distributes member by member; wrapping both sides in tuples suppresses that behavior. Conditional types can defer while a type parameter is unresolved and can become expensive or hard to read when recursively composed.
How do you use the infer keyword in TypeScript?
infer introduces a type variable inside the true branch of a conditional pattern, such as T extends readonly (infer Item)[] ? Item : never. It is useful for function parameters, return types, tuple parts, and nested structures. For promises, prefer the built-in Awaited
Sources
- TypeScript 6.0 announcement
- TypeScript 6.0 release notes
- TypeScript Handbook: Generics
- TypeScript Handbook: keyof
- TypeScript Handbook: Indexed Access Types
- TypeScript Handbook: Conditional Types
- TypeScript Handbook: Mapped Types
- TypeScript Utility Types
- TypeScript 7 native previews
Related Articles
- TypeScript Type vs Interface - When to use type aliases vs interfaces
- TypeScript Tricky Questions - Advanced type system questions
- NestJS Interview Guide - TypeScript decorators and generics in DI
- Complete Frontend Developer Interview Guide - Comprehensive preparation guide
