Skip to content

TypeScript Generics: Basics, Constraints, and Real Patterns

A deep, interview-ready guide to TypeScript generics: why they exist, how inference works, constraints (extends), default generics, and practical examples used in real codebases.

Rajesh DhimanFebruary 08, 2026~20 min
On this page

30‑Second Senior Interview Answer

Generics are parameterized types that allow developers to write reusable, type-safe functions, classes, and interfaces while preserving exact type fidelity across inputs and outputs.

Unlike any (which abandons the type checker) or unknown (which requires manual type narrowing), a generic type variable T captures and propagates the caller's specific type throughout the signature.

At a senior level, mastering generics requires understanding:

  1. Constraints (extends): Restricting the domain of allowable types.
  2. Type Inference: When TypeScript infers type arguments versus when explicit arguments are required.
  3. Variance: Covariance in return types versus contravariance in callback parameters.
  4. Distributive Conditional Types: How naked generic type parameters distribute over union types.

All generic type parameters are strictly compile-time constructs erased during transpilation, incurring zero runtime performance overhead.


1. Type Parameters vs Runtime Arguments (The Mental Model)

Think of generics as functions at the type level:

  • Regular functions accept values as arguments and produce values.
  • Generic types accept types as parameters and produce specialized types.
// Value level:
function double(num: number) { return num * 2; }
 
// Type level:
type Box<T> = { value: T };
type StringBox = Box<string>; // { value: string }
type NumberBox = Box<number>; // { value: number }

Automatic Inference vs Explicit Specification

In most function invocations, TypeScript infers generic parameters from runtime arguments automatically:

function identity<T>(value: T): T {
  return value;
}
 
// Inferred automatically:
const s = identity('hello'); // inferred: identity<string>(value: string): string
const n = identity(42);      // inferred: identity<number>(value: number): number
 
// Explicit override (needed when return type cannot be inferred from arguments):
const parsed = JSON.parse(jsonString) as unknown; // unsafe
async function fetchApi<T>(url: string): Promise<T> {
  const res = await fetch(url);
  return res.json() as Promise<T>;
}
const user = await fetchApi<{ id: string; name: string }>('/api/user');

2. Generic Constraints: The extends Keyword

By default, an unconstrained generic T can be anything (unknown). If you attempt to access properties on T, TypeScript emits a compiler error because it cannot guarantee the property exists:

// ❌ Error: Property 'length' does not exist on type 'T'.
function printLength<T>(item: T) {
  console.log(item.length);
}

Constraining to an Object Shape

Use extends to enforce that T must satisfy a specific structural shape:

interface HasLength {
  length: number;
}
 
function printLength<T extends HasLength>(item: T): number {
  return item.length; // ✅ Guaranteed to exist
}
 
printLength('frontend'); // string has .length -> OK
printLength([1, 2, 3]);   // Array has .length -> OK
printLength({ length: 5, label: 'items' }); // Structural match -> OK
// printLength(100); // ❌ Argument of type 'number' is not assignable to 'HasLength'

Multiple Constraints via Intersections (&)

If a generic parameter must satisfy multiple interfaces:

interface Identifiable { id: string; }
interface Timestamped { createdAt: Date; }
 
function syncEntity<T extends Identifiable & Timestamped>(entity: T): string {
  return `Entity ${entity.id} created at ${entity.createdAt.toISOString()}`;
}

3. keyof + Generics (Type-Safe Property Lookup)

One of the most frequently requested interview implementations is a type-safe getProp helper:

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const developer = {
  name: 'Rajesh Dhiman',
  company: 'Eunix Tech',
  experienceYears: 10,
  skills: ['TypeScript', 'React', 'Next.js'],
};
 
// Return types are precisely inferred:
const devName = getProp(developer, 'name');           // string
const exp = getProp(developer, 'experienceYears');    // number
const skills = getProp(developer, 'skills');          // string[]
 
// ❌ Compile-time Error: Argument of type '"salary"' is not assignable to parameter of type '"name" | "company" | "experienceYears" | "skills"'.
// getProp(developer, 'salary');

4. Variance in Generics: Covariance vs Contravariance

Senior and staff candidates are expected to understand how generic subtyping behaves across inputs and outputs.

Suppose Dog extends Animal:

class Animal { name = 'Animal'; }
class Dog extends Animal { bark() { console.log('Woof'); } }

Rule 1: Output positions are Covariant (Direction is Preserved)

Producer<Dog> is assignable to Producer<Animal>:

type Producer<T> = () => T;
 
let getDog: Producer<Dog> = () => new Dog();
let getAnimal: Producer<Animal> = getDog; // ✅ Allowed! A function returning a Dog satisfies a caller expecting an Animal.

Rule 2: Input positions are Contravariant (Direction is Inverted)

Consumer<Animal> is assignable to Consumer<Dog>:

type Consumer<T> = (item: T) => void;
 
let handleAnimal: Consumer<Animal> = (animal: Animal) => console.log(animal.name);
let handleDog: Consumer<Dog> = handleAnimal; // ✅ Allowed! A handler that accepts ANY Animal can safely accept a Dog.

[!IMPORTANT] Functions are contravariant in their arguments and covariant in their return types.


5. Distributive Conditional Types with Generics

When a conditional type acts on a "naked" generic type parameter, it distributes over union types:

type ToArray<T> = T extends any ? T[] : never;
 
type Mixed = ToArray<string | number>;
// Distributes as: ToArray<string> | ToArray<number>
// Evaluates to: string[] | number[]

If you want to prevent distribution and treat the union as a single entity, wrap the parameter in tuples [T]:

type NonDistributiveToArray<T> = [T] extends [any] ? T[] : never;
 
type Solid = NonDistributiveToArray<string | number>;
// Evaluates to: (string | number)[]

Whiteboard Challenge: Polyfill ReturnType<T>

Using conditional types and the infer keyword inside a generic constraint:

type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;
 
function calculateScore() {
  return { score: 98, rank: 'A+' };
}
 
type ScoreResult = MyReturnType<typeof calculateScore>;
// Inferred as: { score: number; rank: string; }

6. Real-World Production Patterns

Pattern A: Strongly Typed Event Emitter

type EventMap = {
  login: { userId: string; timestamp: number };
  logout: { reason: string };
  error: { code: number; message: string };
};
 
class TypedEventEmitter<Events extends Record<string, any>> {
  private listeners: {
    [K in keyof Events]?: Array<(payload: Events[K]) => void>;
  } = {};
 
  on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event]!.push(handler);
  }
 
  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.listeners[event]?.forEach((handler) => handler(payload));
  }
}
 
// Usage:
const emitter = new TypedEventEmitter<EventMap>();
 
emitter.on('login', (data) => {
  console.log(data.userId); // Autocompleted & verified!
});
 
// ❌ Error: Argument of type '{ wrongKey: true }' is not assignable to parameter of type '{ reason: string }'
// emitter.emit('logout', { wrongKey: true });

Pattern B: Fluent Builder Pattern with Generic State Accumulation

interface RequestConfig<TBody> {
  url: string;
  method: 'GET' | 'POST';
  body?: TBody;
}
 
class RequestBuilder<TBody = void> {
  private config: Partial<RequestConfig<TBody>> = {};
 
  url(url: string): this {
    this.config.url = url;
    return this;
  }
 
  method(method: 'GET' | 'POST'): this {
    this.config.method = method;
    return this;
  }
 
  body<B>(body: B): RequestBuilder<B> {
    const next = new RequestBuilder<B>();
    next.config = { ...this.config, body } as any;
    return next;
  }
 
  build(): RequestConfig<TBody> {
    if (!this.config.url || !this.config.method) {
      throw new Error('Incomplete request config');
    }
    return this.config as RequestConfig<TBody>;
  }
}

7. The Golden Rule of Generics: Avoid Over-Engineering

[!WARNING] The Single-Occurrence Smell: If a type parameter T appears only once in a function's argument list and is not used in the return type or other parameters, you do not need a generic.

// ❌ Unnecessary Generic:
function logId<T extends { id: string }>(item: T): void {
  console.log(item.id);
}
 
// ✅ Clean Structural Typing:
function logId(item: { id: string }): void {
  console.log(item.id);
}

Generics exist to relate multiple values (e.g. input to output, parameter 1 to parameter 2, or class properties to method returns). If no relationship needs preserving, standard interfaces and unions are simpler and cleaner.


Summary Checklist

  • Generics preserve type relationships between inputs and outputs.
  • extends constrains types to specific structural interfaces.
  • keyof T and T[K] provide type-safe object property indexing.
  • Functions are covariant in returns and contravariant in arguments.
  • Naked generics distribute over union types in conditional statements.
  • Avoid generic type variables that appear only once in a function signature.

Written by Rajesh Dhiman at Eunix Tech.

Companion Book

Looking for structured, offline interview prep?

Free online lessons • Portable Amazon Kindle / Paperback

Frontend Interview Guide (2025 Edition) cover
Recommended for React & Frontend Rounds

Frontend Interview Guide (2025 Edition)

By Rajesh Dhiman

Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.

View on Amazon ↗Kindle & Paperback
JavaScript Interview Guide cover
Recommended for Core JS Rounds

JavaScript Interview Guide

By Rajesh Dhiman

Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.

View on Amazon ↗Kindle & Paperback

Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.

RD

Written by Rajesh Dhiman

Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.