45+ TypeScript Generics Interview Questions 2025: Constraints, Utility Types & Patterns

·20 min read
typescriptgenericsfrontendjavascriptinterview-preparation

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.

Table of Contents

  1. Generic Fundamentals Questions
  2. Generic Functions Questions
  3. Generic Constraints Questions
  4. Generic Interfaces and Classes Questions
  5. Utility Types Questions
  6. Conditional Types Questions
  7. Real-World Pattern Questions
  8. Advanced Generics Questions
  9. 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 you write reusable code that works with multiple types while keeping type safety. Instead of using any, you define a type variable like T that captures the actual type being used. This way, a function like identity<T>(arg: T): T returns the same type it receives—if you pass a string, TypeScript knows it returns a string.

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 between generics and any is that generics preserve type information while any discards it entirely. With any, you've told TypeScript "I don't care about types here," and the compiler can no longer help you catch errors.

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 runtime

With 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 clarity

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.

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 length

The extends keyword in T extends { length: number } creates a constraint. It's saying "T must be assignable to an object with a length property."

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 user

The 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 too

How 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 AppState

Utility 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 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 fields

What is Required and when do you use it?

Required<T> does the opposite of Partial—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}`);
}

What is Pick and Omit and how do they differ?

Pick<T, K> creates a type with only the specified properties. Omit<T, K> creates a type without the specified properties. They're 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; }

What is Record and when do you use it?

Record<K, T> creates an object type where all keys of type K map to values of type T. This is 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
};

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]
 
// 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 type

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 string

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)

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 (infer U)[] ? U : never;
 
type A = ElementType<string[]>;   // string
type B = ElementType<number[]>;   // number
type C = ElementType<(string | number)[]>; // string | number

What are distributive conditional types?

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 | boolean

Real-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 typed HTTP client is a common pattern in production applications:

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'
});

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 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', {});

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 when 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.

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 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 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 parser

Quick Reference

What are the key generic concepts and their syntax?

ConceptSyntaxWhen to Use
Basic generic<T>Preserve type through function/class
Multiple params<T, U>Transform between different types
ConstraintT extends XRequire 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
ConditionalT extends U ? X : YChoose type based on condition
Inferinfer UExtract type from structure

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides