Skip to content

TypeScript Advanced Generics — Complete Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Advanced Generics. We cover key concepts, practical examples, and best practices to help you master this topic.

Advanced TypeScript generics go beyond basic type parameters into generic classes, conditional types that transform types based on conditions, mapped types that iterate over unions, and the infer keyword for extracting types from other types.

What You'll Learn

  • Generic classes and factories
  • Conditional types with extends ternary
  • The infer keyword for type extraction
  • Distributive conditional types
  • Mapped types with in keyof
  • Real-world advanced generic patterns

Why It Matters

Basic generics let you reuse code across types. Advanced generics let you compute types at compile time — transforming inputs, extracting return types, making properties optional, or building type-safe builders. This is what separates TypeScript from basic type systems.

Real-World Use

React's ComponentProps type extracts the props type from any component. Prisma's generated types use conditional types to handle optional relations. The Doda Browser extension API uses mapped types to convert every method into a version with and without callbacks (Promise-based).

Learning Path

flowchart LR
  A[Generics Basics] --> B[Advanced Generics]
  B --> C[Conditional Types]
  A --> D[You Are Here]
  C --> E[Mapped Types]
  E --> F[Utility Types]

Generic Classes

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];
  }

  get length(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20
console.log(numberStack.peek()); // 10

const stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.pop()); // b

Generic Factory Functions

class Queue<T> {
  private items: T[] = [];

  enqueue(item: T): void { this.items.push(item); }
  dequeue(): T | undefined { return this.items.shift(); }
}

function createQueue<T>(initialItems?: T[]): Queue<T> {
  const queue = new Queue<T>();
  if (initialItems) {
    for (const item of initialItems) {
      queue.enqueue(item);
    }
  }
  return queue;
}

const queue = createQueue([1, 2, 3]);
console.log(queue.dequeue()); // 1

Conditional Types

Conditional types select one type or another based on a condition:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true (literal extends string)

Think of conditional types like ternary operators for types: T extends U ? X : Y

Real-World Conditional Types

type ExtractPromise<T> = T extends Promise<infer U> ? U : T;

type A = ExtractPromise<Promise<string>>; // string
type B = ExtractPromise<number>;          // number (not a promise)

// Function return type extraction
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R = ReturnOf<Fn>; // string

The infer Keyword

infer lets you declare a type variable inside a conditional type's extends clause, capturing a part of the type:

type ArrayItem<T> = T extends Array<infer U> ? U : T;

type A = ArrayItem<string[]>; // string
type B = ArrayItem<number>;   // number (not an array)

type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;

type Fn = (name: string, age: number) => void;
type Name = FirstArg<Fn>; // string

Deep Infer

type PromiseValue<T> = T extends Promise<infer U>
  ? U extends Promise<infer V>
    ? V
    : U
  : T;

type A = PromiseValue<Promise<Promise<string>>>; // string
type B = PromiseValue<Promise<number>>;           // number

Distributive Conditional Types

When a conditional type acts on a bare generic type parameter, it distributes over unions:

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

type Result = ToArray<string | number>;
// Distributive: string[] | number[] (not (string | number)[])

// Without distribution — wrap in tuple
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]

Filtering with Distributive Types

type ExcludeNull<T> = T extends null | undefined ? never : T;

type Values = string | number | null | undefined;
type NonNull = ExcludeNull<Values>; // string | number

Mapped Types

Mapped types iterate over the keys of an object type to produce a new type:

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Partial<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
}

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }

Mapping with Property Modifiers

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type OptionalExceptId<T> = {
  [K in keyof T as K extends "id" ? K : never]: T[K];
} & {
  [K in keyof T as K extends "id" ? never : K]?: T[K];
};

Template Literal Types with Generics

type EventName<T extends string> = `on${Capitalize<T>}`;
type Events = EventName<"click" | "submit">; // "onClick" | "onSubmit"

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

Common Mistakes

1. Forgetting That Conditional Types Distribute Over Unions

type IsString<T> = T extends string ? true : false;
type Result = IsString<string | number>;
// Distributes: IsString<string> | IsString<number> = true | false = boolean

Wrap in [T] extends [U] to prevent distribution.

2. Overusing infer When a Simpler Type Exists

// Overkill
type ElementType<T> = T extends (infer U)[] ? U : never;

// Already exists as a utility
// T[number] — indexed access
type ElementType<T> = T extends unknown[] ? T[number] : never;

3. Creating Unreadable Nested Conditionals

// Hard to read
type WhatIsThis<T> = T extends string
  ? T extends `#${string}`
    ? "hex-color"
    : "regular-string"
  : T extends number
    ? "number"
    : "other";

// Better with helper types
type HexString<T> = T extends `#${string}` ? true : false;
type WhatIsThis<T> =
  T extends string ? (HexString<T> extends true ? "hex-color" : "regular-string")
  : T extends number ? "number"
  : "other";

4. Recursive Conditionals Without a Base Case

type FlattenDeep<T> = T extends unknown[]
  ? FlattenDeep<T[number]> // No base case — infinite recursion
  : T;

// Fix: add base case for non-array
type FlattenDeep<T> = T extends (infer U)[]
  ? FlattenDeep<U>
  : T;

5. Not Using as in Mapped Types for Key Remapping

TS 4.1+ lets you remap keys with as:

// Without remapping — just same keys
type Without<T> = { [K in keyof T]: T[K] };

// With remapping — transform keys
type WithPrefix<T> = { [K in keyof T as `_${string & K}`]: T[K] };

Practice Questions

  1. What is a conditional type and how is it written? A type that selects between two types based on a condition: T extends U ? X : Y.

  2. What does the infer keyword do? It declares a type variable inside a conditional's extends clause to capture and use a part of the matched type.

  3. What is distributive conditional typing? When a conditional type on a bare generic distributes over union members, applying the condition to each union member individually.

  4. How do you iterate over object keys in a mapped type? Using [K in keyof T]: T[K], where keyof T produces a union of property keys.

Challenge: Write a DeepReadonly<T> mapped type that makes all properties of an object and its nested objects readonly. Use it on a nested configuration object.

FAQ

Do conditional types affect runtime performance?

No. All type computations happen at compile time and are erased in the output JavaScript.

Can I use `infer` in multiple positions?

Yes: type Swap<T> = T extends [infer A, infer B] ? [B, A] : T;

What is the limit of recursive type depth?

TypeScript has a Recursion limit (typically 50 levels deep) to prevent infinite compilation loops.

Can conditional types reference themselves?

Yes, for recursive type definitions like JSON types or deep partial.

What is the difference between mapped and conditional types?

Mapped types iterate over keys ([K in keyof T]) to transform each property. Conditional types branch based on a condition (T extends U ? X : Y).

Mini Project: Type-Safe Event Emitter

// src/event-emitter.ts

type EventMap = Record<string, unknown[]>;

class TypedEventEmitter<T extends EventMap> {
  private listeners: {
    [K in keyof T]?: Array<(...args: T[K]) => void>;
  } = {};

  on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, ...args: T[K]): void {
    const handlers = this.listeners[event];
    if (handlers) {
      for (const handler of handlers) {
        handler(...args);
      }
    }
  }

  off<K extends keyof T>(event: K, listener: (...args: T[K]) => void): void {
    const handlers = this.listeners[event];
    if (handlers) {
      this.listeners[event] = handlers.filter(h => h !== listener) as any;
    }
  }
}

interface AppEvents {
  scanComplete: [scanId: string, threats: number];
  threatDetected: [threatName: string, severity: number];
  error: [message: string];
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.on("scanComplete", (scanId, threats) => {
  console.log(`Scan ${scanId}: ${threats} threats found`);
});

emitter.on("threatDetected", (name, severity) => {
  console.log(`Threat: ${name} (severity: ${severity})`);
});

emitter.emit("scanComplete", "scan-001", 3);
emitter.emit("threatDetected", "Trojan.Generic", 7);
// emitter.emit("scanComplete", 123); // Error — wrong argument types

Expected output:

Scan scan-001: 3 threats found
Threat: Trojan.Generic (severity: 7)

What's Next

Now explore keyof, typeof, and indexed access types:

Lesson Description
{{< ref "/programming-languages/typescript/09-generics-basics" >}} Review generics basics
{{< ref "/programming-languages/typescript/11-keyof-typeof" >}} Keyof, typeof, indexed access types
{{< ref "/programming-languages/typescript/12-template-literal-types" >}} Template literal types

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro