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 needanyor 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 typeT.”Repository<T>describes “something that stores entities of typeT.” 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,Tcould be anything and the class couldn’t safely access any members. The constraint narrows whatTcan be, allowing useful operations.
Generic classes vs interfaces
Both can be generic; they serve different purposes.
| Aspect | Generic class | Generic 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
Tvalues - 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 justMemoryCache<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 typeMemoryRepository<T>class โ a concrete implementation- Constraint
T extends Entityโ ensures every entity has anid - 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
| Form | Example |
|---|---|
| Declaration | class Box<T> { } |
| Multiple | class Pair<A, B> { } |
| Constraint | class Repo<T extends Entity> { } |
| Default | class Result<T, E = Error> { } |
| Instantiation | new Box<number>(42) |
| Inference | new Box(42) |
Generic Interface Syntax
| Form | Example |
|---|---|
| Declaration | interface Container<T> { } |
| Multiple | interface Map<K, V> { } |
| Constraint | interface Repo<T extends Entity> { } |
| Default | interface Result<T, E = Error> { } |
| Class implements | class C implements Container<T> |
| Object literal | const c: Container<number> = { ... } |
Where Type Parameters Apply
| Location | Example |
|---|---|
| Properties | value: T |
| Methods | get(): T |
| Method params | push(item: T) |
| Constructor | constructor(public value: T) |
| Return types | map(): Box<U> |
| Static methods | static of<T>(x: T): Box<T> |
Method-Level Type Parameters
| Form | Example |
|---|---|
| Additional | map<U>(fn: (x: T) => U): U[] |
| Independent of class | wrap<U>(u: U): Box<U> |
| Constrained | find<K extends keyof T>(k: K): T[K] |
Constraints
| Form | Meaning |
|---|---|
<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
| Class | Purpose |
|---|---|
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
| Aspect | Generic | Non-generic |
|---|---|---|
| Reusable | โ | โ |
| Type-safe | โ | โ (specific) |
| Flexible | โ | โ |
| Boilerplate | One class | Class per type |
Standard Library Generic Classes
| Class | Type params |
|---|---|
Array<T> | 1 |
Promise<T> | 1 |
Map<K, V> | 2 |
Set<T> | 1 |
WeakMap<K, V> | 2 |
WeakSet<T> | 1 |
Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
Type 'T' is not assignable to ... | Missing constraint | Add <T extends ...> |
Property 'id' does not exist on type 'T' | No constraint | <T extends { id: ... }> |
Expected N type arguments | Wrong arity | Match parameter count |
Cannot use T as a value | Type vs value | Use a parameter or typeof |
Common Patterns
| Pattern | Signature |
|---|---|
| Wrapper | class Box<T> { value: T } |
| Container | class Stack<T> { items: T[] } |
| Result | class Result<T, E = Error> |
| Repository | class Repo<T extends Entity> |
| Cache | class Cache<K, V> |
| Typed emitter | class Emitter<Events> |
this Return Type in Generics
| Form | Meaning |
|---|---|
method(): this | Returns the current class |
method(): T | Returns 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
| Pitfall | Problem | Solution |
|---|---|---|
| Missing constraint | Can’t access property | Add <T extends ...> |
| Type param unused | No benefit | Remove or use it |
| Shadowed type params | Confusion | Rename |
Can’t instantiate T | Type-only | Pass constructor or factory |
Static + instance T | Statics can’t use instance T | Use own type params in statics |
| Method shadows class | Wrong T | Rename |
| Wrong arity | Missing type args | Match parameter count |
| Over-constrained | Too narrow | Loosen |
T in value position | Runtime missing | Pass 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
| Concept | Meaning |
|---|---|
| Generic class | Class with type parameters |
| Generic interface | Interface with type parameters |
| Type parameter | <T> โ placeholder |
| Constraint | T extends U |
| Default | <T = string> |
| Method type param | <U> on a method |
| Multiple params | <A, B> |
keyof constraint | K extends keyof T |
| Interface + class | Contract + 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)infersT = number - Constraints โ
T extends Uโ let the class rely on properties ofT keyofconstraints 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!