| |

TypeScript 26 ๐Ÿ”ท Generics โ€” Classes and Interfaces

Generics aren’t only for functions. They apply to classes and interfaces too โ€” and that’s where they become foundational. Array<T>, Promise<T>, Map<K, V>, Set<T> are all generic classes or interfaces in TypeScript’s standard library. Once you can write your own, you can build reusable data structures, containers, result types, repositories, and APIs that preserve type relationships across their entire surface. A generic class holds values of a type the caller chooses; a generic interface describes a shape parameterized by one or more types. Together they’re how you write code that works with many types without giving up type safety.

Key point: A generic class declares type parameters on the class itself โ€” class Box<T> โ€” and those parameters flow into every member, method, and constructor. A generic interface declares type parameters on the interface โ€” interface Container<T> โ€” and any class or object that implements it must specify or infer them. Both preserve the relationship between the type parameter and the members that use it. You write the shape once, and it works for any type.


Generic classes

A generic class declares type parameters after the class name.

class Box<T> {
  constructor(public value: T) {}

  get(): T {
    return this.value;
  }

  set(value: T): void {
    this.value = value;
  }
}

const numberBox = new Box(42);          // Box<number>
const stringBox = new Box('hello');     // Box<string>
const userBox = new Box({ id: 1 });     // Box<{ id: number }>

Box<T> is a class parameterized by T. When you write new Box(42), TypeScript infers T = number โ€” the resulting instance is Box<number>.

Explicit type arguments:

const b = new Box<number>(42);          // explicit
const c = new Box<string>('hello');     // explicit

Usually inference is enough. Explicit arguments matter when inference can’t decide.

Where T is available: Every member of the class can use T โ€” properties, methods, constructor parameters, and return types.

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 size(): number {
    return this.items.length;
  }

  map<U>(fn: (item: T) => U): Stack<U> {
    const result = new Stack<U>();
    for (const item of this.items) {
      result.push(fn(item));
    }
    return result;
  }
}

Stack<number> has push(42), pop(): number | undefined. The map<U> method adds its own type parameter โ€” a method can be more generic than its class.

Methods with their own type parameters:

class Mapper<T> {
  constructor(private items: T[]) {}

  map<U>(fn: (item: T) => U): U[] {
    return this.items.map(fn);
  }

  filter(pred: (item: T) => boolean): T[] {
    return this.items.filter(pred);
  }
}

const m = new Mapper([1, 2, 3]);
const strings = m.map(n => `${n}`);   // string[]
const even = m.filter(n => n % 2 === 0);  // number[]

map<U> declares its own U. The class’s T and the method’s U are independent.

Multiple type parameters:

class Pair<A, B> {
  constructor(public first: A, public second: B) {}

  swap(): Pair<B, A> {
    return new Pair(this.second, this.first);
  }
}

const p = new Pair(1, 'a');   // Pair<number, string>
const swapped = p.swap();     // Pair<string, number>

Each type parameter is captured separately and flows through the members that use it.

Why generic classes exist: They let a class hold, manipulate, or return values of a type chosen by the caller. Box<T> can wrap anything; Stack<T> can stack anything; Result<T, E> can represent success or failure of anything. Without generics, each would need any or a class per type. Generics give one class and full typing.


Generic classes in practice

Some patterns show up constantly.

Container / wrapper:

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

  add(item: T): this {
    this.items.push(item);
    return this;
  }

  all(): readonly T[] {
    return this.items;
  }

  find(pred: (item: T) => boolean): T | undefined {
    return this.items.find(pred);
  }
}

const c = new Container<number>()
  .add(1)
  .add(2)
  .add(3);

c.all();    // readonly number[]

The class stores items of type T and exposes operations that respect it.

Result / Option:

class Result<T, E = Error> {
  private constructor(
    private ok: boolean,
    private value?: T,
    private error?: E
  ) {}

  static success<T>(value: T): Result<T, never> {
    return new Result<T, never>(true, value);
  }

  static failure<E>(error: E): Result<never, E> {
    return new Result<never, E>(false, undefined, error);
  }

  isOk(): boolean {
    return this.ok;
  }

  unwrap(): T {
    if (!this.ok) throw this.error;
    return this.value!;
  }

  unwrapOr(fallback: T): T {
    return this.ok ? this.value! : fallback;
  }
}

const r = Result.success(42);
r.unwrap();   // number

The class models “value or error” with the value’s type captured in T and the error’s in E.

Repository:

class Repository<T extends { id: string }> {
  private items = new Map<string, T>();

  save(item: T): void {
    this.items.set(item.id, item);
  }

  findById(id: string): T | undefined {
    return this.items.get(id);
  }

  all(): T[] {
    return [...this.items.values()];
  }

  delete(id: string): boolean {
    return this.items.delete(id);
  }
}

interface User { id: string; name: string; }
interface Product { id: string; price: number; }

const users = new Repository<User>();
const products = new Repository<Product>();

The constraint T extends { id: string } ensures every stored item has an ID. Both User and Product satisfy it.

Type-safe event emitter:

class Emitter<Events extends Record<string, unknown[]>> {
  private handlers = new Map<keyof Events, Function[]>();

  on<K extends keyof Events>(
    event: K,
    handler: (...args: Events[K]) => void
  ): void {
    const list = this.handlers.get(event) ?? [];
    list.push(handler);
    this.handlers.set(event, list);
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.handlers.get(event)?.forEach(h => h(...args));
  }
}

type AppEvents = {
  click: [x: number, y: number];
  keydown: [key: string];
};

const emitter = new Emitter<AppEvents>();
emitter.on('click', (x, y) => console.log(x, y));
emitter.emit('click', 10, 20);

The event names and their argument types are captured in Events. The on and emit methods use keyof Events and Events[K] to be precise.

Why these patterns recur: They’re the fundamental abstractions โ€” containers, results, repositories, emitters. Each holds or manipulates a type chosen by the caller. Generics make them reusable across every data type, and constraints ensure they only work with types that have the right shape.


Generic interfaces

A generic interface declares type parameters that any implementation must satisfy.

interface Container<T> {
  add(item: T): void;
  get(index: number): T | undefined;
  size(): number;
}

Any class with these members for a given T satisfies the interface.

class ArrayContainer<T> implements Container<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  get(index: number): T | undefined {
    return this.items[index];
  }

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

ArrayContainer<number> is a Container<number>. The T is declared on the class and matches the interface’s.

Implementing with a specific type:

class StringContainer implements Container<string> {
  private items: string[] = [];

  add(item: string): void { this.items.push(item); }
  get(index: number): string | undefined { return this.items[index]; }
  size(): number { return this.items.length; }
}

StringContainer fixes T = string. It satisfies Container<string>, not Container<T> for any T.

Generic interface as a function parameter:

function printAll<T>(container: Container<T>): void {
  for (let i = 0; i < container.size(); i++) {
    const item = container.get(i);
    if (item !== undefined) console.log(item);
  }
}

printAll(new ArrayContainer<number>());
printAll(new ArrayContainer<string>());

The function works with any Container<T>.

Optional members and methods:

interface Repository<T> {
  find(id: string): T | undefined;
  save(item: T): void;
  delete?(id: string): void;   // optional
}

The delete method is optional. Implementing classes can provide it or not.

Readonly properties:

interface Box<T> {
  readonly value: T;
}

Read-only properties in generic interfaces work the same way as in non-generic ones.

Index signatures:

interface Dict<K extends string, V> {
  [key: string]: V;
}

A generic index signature โ€” the key type is string, the value type is V.

Why generic interfaces exist: They describe shapes that are parameterized by a type. Container<T> describes “something that holds items of type T.” Repository<T> describes “something that stores entities of type T.” The interface captures the shape once; any class or object that matches it works.


Constraints on generic classes

A generic class can constrain its type parameters with extends.

class EntityService<T extends { id: string }> {
  private items = new Map<string, T>();

  get(id: string): T | undefined {
    return this.items.get(id);
  }

  save(item: T): void {
    this.items.set(item.id, item);
  }
}

T must have an id: string. The class can use item.id safely.

Constraint with keyof:

class Sorter<T, K extends keyof T> {
  constructor(private key: K) {}

  sort(items: T[]): T[] {
    return [...items].sort((a, b) => {
      const av = a[this.key];
      const bv = b[this.key];
      return av < bv ? -1 : av > bv ? 1 : 0;
    });
  }
}

const byName = new Sorter<{ name: string; age: number }, 'name'>('name');
const byAge = new Sorter<{ name: string; age: number }, 'age'>('age');

K is constrained to keyof T. The sort method can safely access a[this.key].

Constraint referencing another type parameter:

class EntityCache<T, K extends keyof T> {
  private cache = new Map<T[K], T>();

  set(key: T[K], value: T): void {
    this.cache.set(key, value);
  }

  get(key: T[K]): T | undefined {
    return this.cache.get(key);
  }
}

const users = new EntityCache<{ id: number; name: string }, 'id'>();
users.set(1, { id: 1, name: 'Alice' });

K extends keyof T โ€” the second type parameter is a key of the first. T[K] is the type of that key’s value.

Default type parameters:

class Result<T, E = Error> {
  constructor(
    public readonly ok: boolean,
    public readonly value?: T,
    public readonly error?: E
  ) {}
}

const r1: Result<number> = new Result(true, 42);           // E = Error
const r2: Result<number, string> = new Result(false, undefined, 'oops');  // E = string

Defaults apply when the type is omitted.

Why constraints on generic classes: They let the class rely on properties of T โ€” id, keys, specific fields. Without a constraint, T could be anything and the class couldn’t safely access any members. The constraint narrows what T can be, allowing useful operations.


Generic classes vs interfaces

Both can be generic; they serve different purposes.

AspectGeneric classGeneric interface
Runtimeโœ… existsโŒ erased
Implementationโœ… hasโŒ none
Instantiableโœ…N/A
Multiple inheritanceโŒ single extendsโœ… multiple implements
Structural typingโŒโœ…
Can describe object literalsโŒโœ…
Can have stateโœ…โŒ

When to use a generic class:

  • You need a runtime object with state and methods
  • You’re building a container, data structure, or service
  • You want a constructor to initialize T values
  • You need instance identity

When to use a generic interface:

  • You’re describing a contract that many classes or objects satisfy
  • You want structural typing โ€” any shape match works
  • You’re typing an object literal’s shape
  • You want to decouple consumers from specific implementations

When to use both:

interface Cache<T> {
  get(key: string): T | undefined;
  set(key: string, value: T): void;
}

class MemoryCache<T> implements Cache<T> {
  private store = new Map<string, T>();

  get(key: string): T | undefined {
    return this.store.get(key);
  }

  set(key: string, value: T): void {
    this.store.set(key, value);
  }
}

The interface is the contract; the class is one implementation. Consumers depend on Cache<T> and can accept any implementation.

Why both patterns matter: The interface decouples consumers from implementation โ€” they accept any Cache<T>, not just MemoryCache<T>. The class provides a concrete implementation. Together they give you flexibility and reuse without locking into a single class.


Common generic class patterns

Reusable patterns you’ll see in real code.

Stack:

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.at(-1); }
  isEmpty(): boolean { return this.items.length === 0; }
  get size(): number { return this.items.length; }
}

Queue:

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

  enqueue(item: T): void { this.items.push(item); }
  dequeue(): T | undefined { return this.items.shift(); }
  peek(): T | undefined { return this.items[0]; }
  get size(): number { return this.items.length; }
}

Pair:

class Pair<A, B> {
  constructor(public first: A, public second: B) {}
  swap(): Pair<B, A> { return new Pair(this.second, this.first); }
  map<C, D>(f: (a: A) => C, g: (b: B) => D): Pair<C, D> {
    return new Pair(f(this.first), g(this.second));
  }
}

Result / Either:

class Result<T, E = Error> {
  private constructor(
    private readonly ok: boolean,
    private readonly value?: T,
    private readonly error?: E
  ) {}

  static ok<T>(v: T): Result<T, never> { return new Result(true, v); }
  static err<E>(e: E): Result<never, E> { return new Result(false, undefined, e); }

  isOk(): boolean { return this.ok; }
  unwrap(): T { if (!this.ok) throw this.error; return this.value!; }
  map<U>(fn: (v: T) => U): Result<U, E> {
    return this.ok ? Result.ok(fn(this.value!)) : (this as unknown as Result<U, E>);
  }
}

Repository:

class Repository<T extends { id: string }> {
  private items = new Map<string, T>();

  save(item: T): void { this.items.set(item.id, item); }
  find(id: string): T | undefined { return this.items.get(id); }
  all(): T[] { return [...this.items.values()]; }
  delete(id: string): boolean { return this.items.delete(id); }
}

Cache:

class Cache<K, V> {
  private store = new Map<K, V>();

  get(key: K): V | undefined { return this.store.get(key); }
  set(key: K, value: V): void { this.store.set(key, value); }
  has(key: K): boolean { return this.store.has(key); }
  clear(): void { this.store.clear(); }
}

Each of these is a shape parameterized by the types it works with.

Why these patterns are worth memorizing: They cover most of what you’ll build with generic classes โ€” containers, wrappers, repositories, caches. Each is small, reusable, and type-safe. Once you know the shapes, adapting them to specific needs is quick.


A full example

A type-safe in-memory database with generic classes and interfaces.

// ============================================
// INTERFACES
// ============================================

interface Entity {
  readonly id: string;
}

interface Repository<T extends Entity> {
  find(id: string): T | undefined;
  findAll(): T[];
  save(entity: T): void;
  delete(id: string): boolean;
}

// ============================================
// GENERIC CLASS IMPLEMENTATION
// ============================================

class MemoryRepository<T extends Entity> implements Repository<T> {
  private store = new Map<string, T>();

  find(id: string): T | undefined {
    return this.store.get(id);
  }

  findAll(): T[] {
    return [...this.store.values()];
  }

  save(entity: T): void {
    this.store.set(entity.id, entity);
  }

  delete(id: string): boolean {
    return this.store.delete(id);
  }

  filter(pred: (entity: T) => boolean): T[] {
    return this.findAll().filter(pred);
  }

  map<U>(fn: (entity: T) => U): U[] {
    return this.findAll().map(fn);
  }

  count(): number {
    return this.store.size;
  }
}

// ============================================
// DOMAIN
// ============================================

interface User extends Entity {
  name: string;
  email: string;
}

interface Product extends Entity {
  name: string;
  price: number;
}

// ============================================
// USAGE
// ============================================

const users = new MemoryRepository<User>();
users.save({ id: 'u-1', name: 'Alice', email: 'alice@example.com' });
users.save({ id: 'u-2', name: 'Bob', email: 'bob@example.com' });

const products = new MemoryRepository<Product>();
products.save({ id: 'p-1', name: 'Keyboard', price: 79.99 });
products.save({ id: 'p-2', name: 'Mouse', price: 39.5 });

// Type-safe queries
const alice = users.find('u-1');
// User | undefined

const allUsers = users.findAll();
// User[]

const emails = users.map(u => u.email);
// string[]

const expensive = products.filter(p => p.price > 50);
// Product[]

// Generic function accepting any repository
function getCount<T extends Entity>(repo: Repository<T>): number {
  return repo.findAll().length;
}

console.log(getCount(users));      // 2
console.log(getCount(products));   // 2

console.log(alice?.name);          // Alice
console.log(emails);               // ['alice@example.com', 'bob@example.com']
console.log(expensive.map(p => p.name));  // ['Keyboard']

What this shows:

  • Repository<T> interface โ€” a contract parameterized by the entity type
  • MemoryRepository<T> class โ€” a concrete implementation
  • Constraint T extends Entity โ€” ensures every entity has an id
  • Generic method map<U> โ€” adds a type parameter beyond the class’s
  • Generic function getCount โ€” works with any repository

Every operation preserves type information. users.find('u-1') returns User | undefined; users.map(u => u.email) returns string[].

Why this shape: It’s a real pattern โ€” an in-memory repository that works for any entity type. The interface defines the contract; the class implements it; consumers use the interface. Generic methods and functions extend the pattern. This is how real applications structure data access with TypeScript.


Complete Example Session

# ============================================
# PART 1: BASIC GENERIC CLASS
# ============================================

cat > box.ts << 'EOF'
class Box<T> {
  constructor(public value: T) {}

  get(): T { return this.value; }
  set(value: T): void { this.value = value; }
}

const n = new Box(42);          // Box<number>
const s = new Box('hello');     // Box<string>

console.log(n.get(), s.get());

n.set(100);
console.log(n.get());
EOF

npx tsc --noEmit box.ts
# (no errors)

# ============================================
# PART 2: STACK
# ============================================

cat > stack.ts << 'EOF'
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 size(): number { return this.items.length; }

  map<U>(fn: (item: T) => U): Stack<U> {
    const result = new Stack<U>();
    for (const item of this.items) result.push(fn(item));
    return result;
  }
}

const nums = new Stack<number>();
nums.push(1);
nums.push(2);
nums.push(3);

console.log(nums.peek(), nums.size);
console.log(nums.pop());

const strs = nums.map(n => `#${n}`);
console.log(strs.peek());
EOF

npx tsc --noEmit stack.ts
# (no errors)

# ============================================
# PART 3: PAIR WITH MULTIPLE PARAMS
# ============================================

cat > pair.ts << 'EOF'
class Pair<A, B> {
  constructor(public first: A, public second: B) {}

  swap(): Pair<B, A> {
    return new Pair(this.second, this.first);
  }
}

const p = new Pair(1, 'a');
const swapped = p.swap();
console.log(p.first, p.second);
console.log(swapped.first, swapped.second);
EOF

npx tsc --noEmit pair.ts
# (no errors)

# ============================================
# PART 4: GENERIC INTERFACE
# ============================================

cat > iface.ts << 'EOF'
interface Container<T> {
  add(item: T): void;
  get(index: number): T | undefined;
  size(): number;
}

class ArrayContainer<T> implements Container<T> {
  private items: T[] = [];

  add(item: T): void { this.items.push(item); }
  get(index: number): T | undefined { return this.items[index]; }
  size(): number { return this.items.length; }
}

function printAll<T>(c: Container<T>): void {
  for (let i = 0; i < c.size(); i++) {
    console.log(c.get(i));
  }
}

const c = new ArrayContainer<string>();
c.add('a');
c.add('b');
printAll(c);
EOF

npx tsc --noEmit iface.ts
# (no errors)

# ============================================
# PART 5: CONSTRAINTS
# ============================================

cat > constraints.ts << 'EOF'
interface Entity { id: string; }

class Repository<T extends Entity> {
  private items = new Map<string, T>();

  save(item: T): void { this.items.set(item.id, item); }
  find(id: string): T | undefined { return this.items.get(id); }
  all(): T[] { return [...this.items.values()]; }
}

interface User { id: string; name: string; }

const users = new Repository<User>();
users.save({ id: 'u-1', name: 'Alice' });
console.log(users.find('u-1'));
console.log(users.all());
EOF

npx tsc --noEmit constraints.ts
# (no errors)

# ============================================
# PART 6: RESULT CLASS
# ============================================

cat > result.ts << 'EOF'
class Result<T, E = Error> {
  private constructor(
    public readonly ok: boolean,
    public readonly value?: T,
    public readonly error?: E
  ) {}

  static ok<T>(v: T): Result<T, never> {
    return new Result<T, never>(true, v);
  }

  static err<E>(e: E): Result<never, E> {
    return new Result<never, E>(false, undefined, e);
  }

  unwrap(): T {
    if (!this.ok) throw this.error;
    return this.value!;
  }

  unwrapOr(fallback: T): T {
    return this.ok ? this.value! : fallback;
  }

  map<U>(fn: (v: T) => U): Result<U, E> {
    return this.ok
      ? Result.ok(fn(this.value!))
      : (this as unknown as Result<U, E>);
  }
}

const r = Result.ok(42);
console.log(r.unwrap());
console.log(r.map(n => n * 2).unwrap());

const e = Result.err('failed');
console.log(e.unwrapOr(0));
EOF

npx tsc --noEmit result.ts
# (no errors)

# ============================================
# PART 7: COMPILE AND RUN
# ============================================

npx tsc box.ts stack.ts pair.ts iface.ts constraints.ts result.ts
node box.js
# [ 42 hello ]
# [ 100 ]

node stack.js
# [ 3 3 ]
# [ 3 ]
# [ #2 ]

node pair.js
# [ 1 a ]
# [ a 1 ]

node iface.js
# [ a ]
# [ b ]

node constraints.js
# [ { id: 'u-1', name: 'Alice' } ]
# [ [ { id: 'u-1', name: 'Alice' } ] ]

node result.js
# [ 42 ]
# [ 84 ]
# [ 0 ]

Quick Reference

Generic Class Syntax

FormExample
Declarationclass Box<T> { }
Multipleclass Pair<A, B> { }
Constraintclass Repo<T extends Entity> { }
Defaultclass Result<T, E = Error> { }
Instantiationnew Box<number>(42)
Inferencenew Box(42)

Generic Interface Syntax

FormExample
Declarationinterface Container<T> { }
Multipleinterface Map<K, V> { }
Constraintinterface Repo<T extends Entity> { }
Defaultinterface Result<T, E = Error> { }
Class implementsclass C implements Container<T>
Object literalconst c: Container<number> = { ... }

Where Type Parameters Apply

LocationExample
Propertiesvalue: T
Methodsget(): T
Method paramspush(item: T)
Constructorconstructor(public value: T)
Return typesmap(): Box<U>
Static methodsstatic of<T>(x: T): Box<T>

Method-Level Type Parameters

FormExample
Additionalmap<U>(fn: (x: T) => U): U[]
Independent of classwrap<U>(u: U): Box<U>
Constrainedfind<K extends keyof T>(k: K): T[K]

Constraints

FormMeaning
<T extends U>T must be assignable to U
<T extends object>T is an object type
<K extends keyof T>K is a key of T
<T extends Entity>T has Entity’s shape
<T, K extends keyof T>K references T

Common Generic Classes

ClassPurpose
Box<T>Wrapper
Stack<T>LIFO
Queue<T>FIFO
Pair<A, B>Two values
Result<T, E>Success or failure
Repository<T>Store
Cache<K, V>Key-value store
Emitter<Events>Typed events

Generic vs Non-Generic

AspectGenericNon-generic
Reusableโœ…โŒ
Type-safeโœ…โœ… (specific)
Flexibleโœ…โŒ
BoilerplateOne classClass per type

Standard Library Generic Classes

ClassType params
Array<T>1
Promise<T>1
Map<K, V>2
Set<T>1
WeakMap<K, V>2
WeakSet<T>1

Errors and Fixes

ErrorCauseFix
Type 'T' is not assignable to ...Missing constraintAdd <T extends ...>
Property 'id' does not exist on type 'T'No constraint<T extends { id: ... }>
Expected N type argumentsWrong arityMatch parameter count
Cannot use T as a valueType vs valueUse a parameter or typeof

Common Patterns

PatternSignature
Wrapperclass Box<T> { value: T }
Containerclass Stack<T> { items: T[] }
Resultclass Result<T, E = Error>
Repositoryclass Repo<T extends Entity>
Cacheclass Cache<K, V>
Typed emitterclass Emitter<Events>

this Return Type in Generics

FormMeaning
method(): thisReturns the current class
method(): TReturns the type parameter
static of<T>(x: T): Box<T>Static factory

Best Practices

โœ… Do This:

// Use generics for containers
class Box<T> {
  constructor(public value: T) {}
}                                                          // โœ…

// Constrain when you need properties
class Repo<T extends { id: string }> { }                   // โœ…

// Provide sensible defaults
class Result<T, E = Error> { }                             // โœ…

// Use `keyof` for type-safe keys
class Sorter<T, K extends keyof T> { }                     // โœ…

// Methods can add their own type parameters
class Stack<T> {
  map<U>(fn: (item: T) => U): Stack<U> { }
}                                                          // โœ…

// Interface + class for contract + implementation
interface Cache<T> { }
class MemoryCache<T> implements Cache<T> { }               // โœ…

// Use `readonly` for exposure
get all(): readonly T[] { return this.items; }             // โœ…

// Explicit type args when inference can't decide
const empty = new Stack<number>();                         // โœ…

โŒ Don’t Do This:

// Don't use `any` for a container
class Box {
  constructor(public value: any) {}
}                                                          // โš ๏ธ

// Don't forget constraints when using properties
class Repo<T> {
  save(item: T) { console.log(item.id); }  // โŒ               // โŒ
}

// Don't shadow class type parameters with method ones
class C<T> {
  method<T>() { }  // โš ๏ธ  shadows the class T                 // โš ๏ธ
}

// Don't over-constrain
class Box<T extends string | number> { }  // โš ๏ธ  narrow         // โš ๏ธ

// Don't mix value and type positions
const x: T = 5;  // โŒ T is a type, not a value              // โŒ

// Don't use generics when a specific type works
class UserBox<T extends User> { }  // โš ๏ธ  just use User        // โš ๏ธ

// Don't forget defaults when omitting args
const r: Result<number> = new Result(true);  // โš ๏ธ  E defaults  // โš ๏ธ

// Don't add type parameters nobody uses
class Thing<T> { value = 0; }  // โš ๏ธ  unused T                 // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Missing constraintCan’t access propertyAdd <T extends ...>
Type param unusedNo benefitRemove or use it
Shadowed type paramsConfusionRename
Can’t instantiate TType-onlyPass constructor or factory
Static + instance TStatics can’t use instance TUse own type params in statics
Method shadows classWrong TRename
Wrong arityMissing type argsMatch parameter count
Over-constrainedToo narrowLoosen
T in value positionRuntime missingPass value separately

Real-World Examples

1. Box wrapper

class Box<T> {
  constructor(public value: T) {}
}

2. Stack

class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

3. Queue

class Queue<T> {
  private items: T[] = [];
  enqueue(i: T): void { this.items.push(i); }
  dequeue(): T | undefined { return this.items.shift(); }
}

4. Pair

class Pair<A, B> {
  constructor(public first: A, public second: B) {}
  swap(): Pair<B, A> { return new Pair(this.second, this.first); }
}

5. Result

class Result<T, E = Error> {
  private constructor(public ok: boolean, public value?: T, public error?: E) {}
  static ok<T>(v: T): Result<T, never> { return new Result(true, v); }
  static err<E>(e: E): Result<never, E> { return new Result(false, undefined, e); }
}

6. Repository with constraint

class Repository<T extends { id: string }> {
  private items = new Map<string, T>();
  save(item: T): void { this.items.set(item.id, item); }
}

7. Cache

class Cache<K, V> {
  private store = new Map<K, V>();
  get(k: K): V | undefined { return this.store.get(k); }
  set(k: K, v: V): void { this.store.set(k, v); }
}

8. Typed event emitter

class Emitter<Events extends Record<string, unknown[]>> {
  on<K extends keyof Events>(e: K, h: (...args: Events[K]) => void): void { }
  emit<K extends keyof Events>(e: K, ...args: Events[K]): void { }
}

9. Method with own type param

class Mapper<T> {
  constructor(private items: T[]) {}
  map<U>(fn: (item: T) => U): U[] { return this.items.map(fn); }
}

10. Sorter with keyof

class Sorter<T, K extends keyof T> {
  constructor(private key: K) {}
  sort(items: T[]): T[] { return [...items].sort((a, b) => a[this.key] < b[this.key] ? -1 : 1); }
}

11. Generic interface

interface Container<T> {
  add(item: T): void;
  get(index: number): T | undefined;
  size(): number;
}

12. Class implements interface

class ArrayContainer<T> implements Container<T> {
  private items: T[] = [];
  add(item: T): void { this.items.push(item); }
  get(i: number): T | undefined { return this.items[i]; }
  size(): number { return this.items.length; }
}

13. Generic function over interface

function printAll<T>(c: Container<T>): void {
  for (let i = 0; i < c.size(); i++) console.log(c.get(i));
}

14. Default type parameter

class ApiResponse<T, E = Error> {
  constructor(public data?: T, public error?: E) {}
}

15. Multiple constraints

class Service<T extends { id: string }, U extends { id: string }> { }

16. Static factory

class Box<T> {
  static of<T>(value: T): Box<T> { return new Box(value); }
  private constructor(public value: T) {}
}

17. Readonly exposure

class Container<T> {
  private items: T[] = [];
  get all(): readonly T[] { return this.items; }
}

18. Filter method

class List<T> {
  constructor(private items: T[]) {}
  filter(pred: (item: T) => boolean): T[] {
    return this.items.filter(pred);
  }
}

19. Chained generic methods

class Query<T> {
  private items: T[] = [];
  where(pred: (t: T) => boolean): Query<T> {
    this.items = this.items.filter(pred);
    return this;
  }
}

20. Generic interface with methods

interface Comparator<T> {
  compare(a: T, b: T): number;
}

Visual: Generic Class

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Box<T> {                              โ”‚
โ”‚    constructor(public value: T) {}           โ”‚
โ”‚    get(): T { return this.value; }           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                   โ”‚
        โ–ผ                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Box<number>  โ”‚   โ”‚  Box<string>  โ”‚
โ”‚               โ”‚   โ”‚               โ”‚
โ”‚  value: num   โ”‚   โ”‚  value: str   โ”‚
โ”‚  get(): num   โ”‚   โ”‚  get(): str   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Type Parameter Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Stack<T> {                            โ”‚
โ”‚    private items: T[] = [];                  โ”‚
โ”‚    push(item: T): void { }                   โ”‚
โ”‚    pop(): T | undefined { }                  โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  T flows through:                            โ”‚
โ”‚  โ”€ property type                             โ”‚
โ”‚  โ”€ method parameters                         โ”‚
โ”‚  โ”€ return types                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  new Stack<number>()
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  All T become number                         โ”‚
โ”‚                                              โ”‚
โ”‚  items: number[]                             โ”‚
โ”‚  push(item: number): void                    โ”‚
โ”‚  pop(): number | undefined                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Generic Interface + Class

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface Container<T> {                    โ”‚
โ”‚    add(item: T): void;                       โ”‚
โ”‚    get(index: number): T | undefined;        โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ† contract                                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ–ฒ
                  โ”‚ implements
                  โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class ArrayContainer<T>                     โ”‚
โ”‚    implements Container<T> {                 โ”‚
โ”‚                                              โ”‚
โ”‚    add(item: T): void { }                    โ”‚
โ”‚    get(i: number): T | undefined { }         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ† implementation                            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Constraints

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Repo<T extends { id: string }> {      โ”‚
โ”‚    private items = new Map<string, T>();     โ”‚
โ”‚                                              โ”‚
โ”‚    save(item: T): void {                     โ”‚
โ”‚      this.items.set(item.id, item);          โ”‚
โ”‚      //              โ†‘                       โ”‚
โ”‚      //  safe because of constraint          โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Without constraint:                         โ”‚
โ”‚                                              โ”‚
โ”‚  class Repo<T> {                             โ”‚
โ”‚    save(item: T) {                           โ”‚
โ”‚      item.id;  โŒ T has no id               โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Method-Level Type Parameter

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Stack<T> {                            โ”‚
โ”‚    private items: T[] = [];                  โ”‚
โ”‚                                              โ”‚
โ”‚    map<U>(fn: (item: T) => U): Stack<U> {    โ”‚
โ”‚    //  โ†‘              โ†‘         โ†‘            โ”‚
โ”‚    //  new type       uses T    returns U    โ”‚
โ”‚      const r = new Stack<U>();               โ”‚
โ”‚      for (const i of this.items) r.push(fn(i));โ”‚
โ”‚      return r;                               โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Stack<T> โ†’ map โ†’ Stack<U>                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Common Generic Containers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Box<T>              โ€” wrap one value        โ”‚
โ”‚  Stack<T>            โ€” LIFO                  โ”‚
โ”‚  Queue<T>            โ€” FIFO                  โ”‚
โ”‚  Pair<A, B>          โ€” two values            โ”‚
โ”‚  Result<T, E>        โ€” success or failure    โ”‚
โ”‚  Repository<T>       โ€” entity store          โ”‚
โ”‚  Cache<K, V>         โ€” key-value store       โ”‚
โ”‚  Emitter<Events>     โ€” typed events          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: keyof in Generic Class

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Sorter<T, K extends keyof T> {        โ”‚
โ”‚    constructor(private key: K) {}            โ”‚
โ”‚                                              โ”‚
โ”‚    sort(items: T[]): T[] {                   โ”‚
โ”‚      return [...items].sort((a, b) => {      โ”‚
โ”‚        const av = a[this.key];               โ”‚
โ”‚        const bv = b[this.key];               โ”‚
โ”‚        return av < bv ? -1 : 1;              โ”‚
โ”‚      });                                     โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  K is a key of T, T[K] is the value type     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  new Sorter<{ name: string }, 'name'>('name')โ”‚
โ”‚  new Sorter<{ age: number }, 'age'>('age')   โ”‚
โ”‚  new Sorter<{ name: string }, 'age'>('age')  โ”‚
โ”‚  // โŒ 'age' not a key                        โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Generic vs Non-Generic

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Non-generic โ€” one per type                  โ”‚
โ”‚                                              โ”‚
โ”‚  class NumberBox { value: number }           โ”‚
โ”‚  class StringBox { value: string }           โ”‚
โ”‚  class UserBox { value: User }               โ”‚
โ”‚                                              โ”‚
โ”‚  Duplication                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Generic โ€” one class, all types              โ”‚
โ”‚                                              โ”‚
โ”‚  class Box<T> { value: T }                   โ”‚
โ”‚                                              โ”‚
โ”‚  new Box(42)      โ†’ Box<number>              โ”‚
โ”‚  new Box('hello') โ†’ Box<string>              โ”‚
โ”‚  new Box(user)    โ†’ Box<User>                โ”‚
โ”‚                                              โ”‚
โ”‚  One class                                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Static vs Instance Type Params

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Instance method โ€” can use class T           โ”‚
โ”‚                                              โ”‚
โ”‚  class Box<T> {                              โ”‚
โ”‚    constructor(public value: T) {}           โ”‚
โ”‚    get(): T { return this.value; }           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โœ… T available                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Static method โ€” can't use class T           โ”‚
โ”‚                                              โ”‚
โ”‚  class Box<T> {                              โ”‚
โ”‚    static create(value: T) { }  โŒ           โ”‚
โ”‚    //             โ†‘                          โ”‚
โ”‚    //  class T not in static scope           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Static needs its own type param:            โ”‚
โ”‚                                              โ”‚
โ”‚  static create<U>(value: U): Box<U> { }      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Decision Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Need a runtime object with state?           โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Generic class              โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ No โ”€โ”€โ–บ Just a shape?               โ”‚
โ”‚                   โ”‚                          โ”‚
โ”‚                   โ”œโ”€โ”€ Yes โ”€โ”€โ–บ Generic        โ”‚
โ”‚                   โ”‚            interface     โ”‚
โ”‚                   โ”‚                          โ”‚
โ”‚                   โ””โ”€โ”€ No โ”€โ”€โ–บ Specific type   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Need both contract and implementation?      โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ Yes โ”€โ”€โ–บ Generic interface +        โ”‚
โ”‚                    generic class             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
Generic classClass with type parameters
Generic interfaceInterface with type parameters
Type parameter<T> โ€” placeholder
ConstraintT extends U
Default<T = string>
Method type param<U> on a method
Multiple params<A, B>
keyof constraintK extends keyof T
Interface + classContract + implementation

Key takeaways:

  • Generic classes declare type parameters that flow into properties, methods, and constructors
  • Generic interfaces describe shapes parameterized by types
  • Inference works for classes too โ€” new Box(42) infers T = number
  • Constraints โ€” T extends U โ€” let the class rely on properties of T
  • keyof constraints ensure a type parameter is a key of another
  • Method-level type parameters add types beyond the class’s
  • Defaults โ€” <T = Error> โ€” apply when types are omitted
  • Static methods can’t use class type parameters โ€” they need their own
  • Interface + class together give you a contract and an implementation
  • Common patterns โ€” Box, Stack, Queue, Pair, Result, Repository, Cache
  • Standard library types like Array<T>, Promise<T>, Map<K, V> follow the same pattern
  • Use generics for containers, wrappers, and reusable structures โ€” not for one-off shapes

Remember: Generic classes and interfaces extend the idea of generics from functions to types you build. A Box<T> wraps anything; a Repository<T> stores anything with an ID; a Result<T, E> represents any operation’s outcome. The type parameter flows through every member, and the caller chooses what T is. That’s how the standard library builds Array, Map, and Promise โ€” and how you build your own reusable data structures with the same level of type safety.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!