Typescript · Generics

Generics Interview Questions
With Answers & Code Examples

7 carefully curated Generics interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
7 questions1 beginner4 core2 advanced
Q1Beginner

What are generics and why are they useful in TypeScript?

💡 Hint: Type parameters that make functions/classes/interfaces reusable across multiple types while preserving type safety

Generics are type parameters that let you write reusable code that works with multiple types without sacrificing type safety.

// Without generics — must duplicate or use any
function getFirstAny(arr: any[]): any { return arr[0]; }

// With generics — type-safe and reusable
function getFirst<T>(arr: T[]): T { return arr[0]; }

const n = getFirst([1, 2, 3]);    // T inferred as number
const s = getFirst(['a', 'b']);   // T inferred as string
// n and s have correct types — no runtime casting needed

Generic interfaces and types:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UserResponse  = ApiResponse<User>;
type ListResponse  = ApiResponse<User[]>;

// Generic 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[this.items.length - 1]; }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push('a'); // ❌ Error — string not assignable to number
💡 Think of generics as function parameters for types. Just as function parameters make a function reusable for different values, type parameters make types reusable for different types.
Practice this question →
Q2Core

What are generic constraints and how do you use the extends keyword with them?

💡 Hint: extends limits which types a type parameter can be — ensures the type has the properties you need

Generic constraints restrict a type parameter to a subset of types using extends. This lets you access properties guaranteed to exist.

// Without constraint — T could be anything
function getLength<T>(val: T) {
  return val.length; // ❌ Error — T might not have .length
}

// With constraint — T must have .length
function getLength<T extends { length: number }>(val: T): number {
  return val.length; // ✅ Safe
}

getLength('hello');      // string has .length
getLength([1, 2, 3]);    // array has .length
getLength(42);           // ❌ Error — number has no .length

keyof constraint — ensures a key exists on an object:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]; // type-safe property access
}

const user = { name: 'Alice', age: 30 };
getProperty(user, 'name');  // returns string
getProperty(user, 'age');   // returns number
getProperty(user, 'email'); // ❌ Error — 'email' not in user

Multiple constraints:

// Intersection for multiple requirements
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

// Constraint with another type parameter
function copyFields<T extends U, U>(target: T, source: U): T {
  return Object.assign(target, source);
}
💡 The pattern <T extends keyof U> is one of the most powerful in TypeScript — it lets you write utilities like Omit, Pick, and getProperty that are fully type-safe at the call site.
Practice this question →
Q3Core

What are default type parameters in TypeScript generics?

💡 Hint: Like default function parameters — a fallback type when the type argument is not specified

Default type parameters provide a fallback type when the caller doesn't explicitly provide one.

// Without default — must always specify T
interface Event<T> {
  type: string;
  payload: T;
}

// With default — T falls back to unknown when not specified
interface Event<T = unknown> {
  type: string;
  payload: T;
}

const genericEvent: Event = { type: 'click', payload: { x: 10 } };
// T inferred as unknown — must narrow payload before using

const typedEvent: Event<MouseData> = { type: 'click', payload: mouseData };
// T is MouseData — payload is fully typed

Practical use cases:

// Promise default is unknown in strict mode, any otherwise
// Custom Result type with default error type
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

function fetchUser(): Promise<Result<User>> {
  // Result<User, Error> — E defaults to Error
}

// Component props with optional extension
interface TableProps<T = Record<string, unknown>> {
  data: T[];
  columns: Array<keyof T>;
}

const table: TableProps = { data: [{}], columns: [] };
// T defaults to Record<string, unknown>
💡 Defaults must come after non-default type parameters, just like JavaScript default arguments. <T, E = Error> is valid; <T = Error, E> is not.
Practice this question →
Q4Core

What is conditional generic typing — how do you write T extends U ? X : Y?

💡 Hint: Distribute over union members; resolve to different types based on whether T satisfies U

Conditional types select a type based on a condition, similar to a ternary operator for types.

// Basic syntax: T extends U ? TypeIfTrue : TypeIfFalse
type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>;  // 'yes'
type B = IsString<number>;  // 'no'

Distribution over union types — when T is a naked type parameter, conditional types distribute:

type ToArray<T> = T extends unknown ? T[] : never;

type StringOrNumberArray = ToArray<string | number>;
// Distributes: (string extends unknown ? string[] : never) | (number extends unknown ? number[] : never)
// = string[] | number[]

// Prevent distribution with []
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type NonDist = ToArrayNonDist<string | number>; // (string | number)[]

infer keyword — extract a type within a conditional:

// Extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type NumReturn = ReturnType<() => number>; // number
type VoidReturn = ReturnType<() => void>;  // void

// Extract element type from an array
type ElementType<T> = T extends (infer E)[] ? E : never;

type Elem = ElementType<string[]>; // string
💡 Conditional types power most of TypeScript's built-in utility types. Understanding infer unlocks writing your own ReturnType, Parameters, PromiseType, and similar utilities.
Practice this question →
Q5Advanced

How do generic functions differ from generic types? Show examples of each.

💡 Hint: Generic functions infer T at the call site; generic types require you to pass T explicitly (or let the constructor infer)

Both use type parameters, but they're instantiated at different points and in different ways.

Generic functions — T is inferred from the argument types at each call:

// Arrow function generic
const wrap = <T>(value: T): { value: T } => ({ value });

const wrapped = wrap(42);        // T inferred as number
const wrapped2 = wrap('hello'); // T inferred as string

// Multiple type params
function zip<A, B>(a: A[], b: B[]): [A, B][] {
  return a.map((item, i) => [item, b[i]]);
}

const pairs = zip([1, 2], ['a', 'b']); // [number, string][]

Generic types/interfaces — T must be provided when using the type:

type Box<T> = { value: T; label: string };

const numBox: Box<number> = { value: 42, label: 'count' };
// Must specify — TS can't infer from usage alone

// Generic class — T provided at instantiation or inferred from constructor
class Queue<T> {
  private items: T[] = [];
  enqueue(item: T): void { this.items.push(item); }
  dequeue(): T | undefined { return this.items.shift(); }
}

const q = new Queue<string>();   // explicit
const q2 = new Queue();           // T inferred as unknown without explicit arg
q.enqueue('hello');
q2.enqueue(42); // T inferred as number from first enqueue
💡 Generic function inference is powerful because callers rarely need to write <T> explicitly — TypeScript figures it out. Only specify the type parameter when inference fails or you want to constrain the result type.
Practice this question →
Q6Advanced

What is the Partial, Required, and Readonly pattern when building generic utility functions?

💡 Hint: Combine utility types in function signatures to produce modified shapes — common in config and builder patterns

Generic utility functions compose TypeScript's built-in modifiers to create flexible, type-safe APIs.

// Deep partial — makes all nested properties optional
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

function mergeDefaults<T extends object>(
  defaults: T,
  overrides: DeepPartial<T>
): T {
  return { ...defaults, ...overrides } as T;
}

const config = mergeDefaults(
  { host: 'localhost', port: 3000, ssl: false },
  { port: 8080 } // only override port — others keep defaults
);
// Update function — require at least one field
type AtLeastOne<T> = { [K in keyof T]-?: Pick<T, K> & Partial<T> }[keyof T];

function update<T extends object>(id: number, patch: Partial<T>): void {
  // ...apply patch
}

// Freeze function — returns readonly version
function freeze<T extends object>(obj: T): Readonly<T> {
  return Object.freeze(obj);
}

const settings = freeze({ theme: 'dark', lang: 'en' });
settings.theme = 'light'; // ❌ Error — readonly
💡 The pattern Partial<T> for update functions and Required<T> for build/create functions is idiomatic TypeScript. It prevents passing full objects when you only need to patch a few fields.
Practice this question →
Q7Core

How do you constrain generic functions to work only with specific shapes using interfaces?

💡 Hint: Extend from an interface or inline shape — ensures the generic value has the properties you need

Combining generics with interface constraints lets you write functions that accept any compatible type while retaining full type information.

// Constraint via interface
interface HasId {
  id: number | string;
}

function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
  return items.find(item => item.id === id);
}

// Works with any object that has an id field
const user = findById(users, 1);     // T = User
const post = findById(posts, 'abc'); // T = Post
// Return type is User | undefined and Post | undefined respectively
// Multiple interface constraints (intersection)
interface Serializable { serialize(): string }
interface Comparable<T> { compareTo(other: T): number }

function sortAndSerialize<T extends Serializable & Comparable<T>>(items: T[]): string[] {
  return items
    .sort((a, b) => a.compareTo(b))
    .map(item => item.serialize());
}

// Constraint + keyof for safe property access
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key]);
}

const names = pluck(users, 'name'); // string[]
const ids = pluck(users, 'id');     // number[]
💡 T extends HasId is more flexible than accepting HasId[] directly — it preserves the full type of the items, so the return type is T | undefined not just HasId | undefined.
Practice this question →

Other Typescript Interview Topics

Rendering Strategies →Core JS →Type System →React Fundamentals →Functions →Microfrontends →Async JS →Hooks →Objects →Monorepo →Arrays →'this' Keyword →Utility Types →Error Handling →Modern JS →Bundle Optimization →Performance →DOM & Events →State Management →Classes & OOP →Caching Strategies →Component Patterns →Advanced Types →Authentication →React Router →Forms →Advanced Patterns →Frontend Security →Concurrent React →Server Components →Testing →Ecosystem →Network Optimization →Core Web Vitals →Browser APIs →

Ready to practice Generics?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →