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 StrategiesCore JSType SystemReact FundamentalsFunctionsMicrofrontendsAsync JSHooksObjectsMonorepoArrays'this' KeywordUtility TypesError HandlingModern JSBundle OptimizationPerformanceDOM & EventsState ManagementClasses & OOPCaching StrategiesComponent PatternsAdvanced TypesAuthenticationReact RouterFormsAdvanced PatternsFrontend SecurityConcurrent ReactServer ComponentsTestingEcosystemNetwork OptimizationCore Web VitalsBrowser APIs

Ready to practice Generics?

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

Start Free Practice →