Generic Interfaces & Classes¶
Generics allow interfaces and classes to adapt to whatever types you pass in—while preserving full type information. This is the foundation of:
- typed collections
- repositories
- services
- adapters
- data loaders
- caching layers
- plugin systems
Let’s explore how they work.
Generic Interfaces¶
A generic interface is an interface with one or more type parameters.
Example: A generic Result<T>¶
interface Result<T> {
ok: boolean
value: T
}
Usage:
const r1: Result<number> = { ok: true, value: 42 }
const r2: Result<string> = { ok: true, value: "hello" }
The interface adapts to whatever type you pass in.
Reusable Data Structures¶
Generic interfaces are perfect for modeling reusable containers.
Example: A typed key/value store¶
interface KeyValueStore<K, V> {
set(key: K, value: V): void
get(key: K): V | undefined
}
Usage:
const store: KeyValueStore<string, number> = {
set(key, value) { /* ... */ },
get(key) { return 123 }
}
Example: A generic API response¶
interface ApiResponse<T> {
status: number
data: T
}
Usage:
type User = { id: number; name: string }
const res: ApiResponse<User> = {
status: 200,
data: { id: 1, name: "Alice" }
}
This pattern is everywhere in real TypeScript codebases.
Generic Classes¶
Classes can also take type parameters.
Example: A generic Queue<T>¶
class Queue<T> {
private items: T[] = []
enqueue(item: T) {
this.items.push(item)
}
dequeue(): T | undefined {
return this.items.shift()
}
}
Usage:
const numberQueue = new Queue<number>()
numberQueue.enqueue(10)
const stringQueue = new Queue<string>()
stringQueue.enqueue("hello")
Each instance is strongly typed.
Generic Constraints in Classes¶
You can restrict what types a class accepts.
Example: Only objects with an id¶
class Repository<T extends { id: string }> {
private items: T[] = []
add(item: T) {
this.items.push(item)
}
find(id: string): T | undefined {
return this.items.find(i => i.id === id)
}
}
Usage:
type User = { id: string; name: string }
const repo = new Repository<User>()
repo.add({ id: "1", name: "Alice" })
Trying to use an incompatible type:
new Repository<number>() // ❌ Error: number does not have id
Constraints keep your abstractions safe.
Generic Services¶
Generics shine when building reusable service layers.
Example: A generic data loader¶
class DataLoader<T> {
constructor(private loadFn: () => Promise<T>) {}
load(): Promise<T> {
return this.loadFn()
}
}
Usage:
const userLoader = new DataLoader(async () => {
return { id: 1, name: "Alice" }
})
const user = await userLoader.load() // T = { id: number; name: string }
Example: A generic caching service¶
class Cache<T> {
private store = new Map<string, T>()
set(key: string, value: T) {
this.store.set(key, value)
}
get(key: string): T | undefined {
return this.store.get(key)
}
}
Usage:
const userCache = new Cache<{ id: number; name: string }>()
userCache.set("1", { id: 1, name: "Alice" })
Example: A generic HTTP client¶
class HttpClient {
async get<T>(url: string): Promise<T> {
const res = await fetch(url)
return res.json() as T
}
}
Usage:
type Product = { id: number; title: string }
const client = new HttpClient()
const product = await client.get<Product>("/api/product")
This is the pattern used by Axios, Prisma, and many modern libraries.
Combining Generic Interfaces & Classes¶
You can use a generic interface to define a contract, and a generic class to implement it.
Example: Repository pattern¶
interface Repository<T> {
add(item: T): void
findById(id: string): T | undefined
}
class MemoryRepository<T extends { id: string }>
implements Repository<T> {
private items: T[] = []
add(item: T) {
this.items.push(item)
}
findById(id: string) {
return this.items.find(i => i.id === id)
}
}
Usage:
type Order = { id: string; total: number }
const orders = new MemoryRepository<Order>()
orders.add({ id: "o1", total: 99 })
This is a fully typed, reusable, extensible data layer.
Summary¶
In this lesson, you learned how generics power reusable abstractions:
1. Generic interfaces¶
- reusable containers
- typed API responses
- flexible data models
2. Generic classes¶
- typed collections
- repositories
- queues, stacks, caches
3. Generic services¶
- loaders
- HTTP clients
- caching layers
- dependency‑injected components
Generics let you design systems that are both flexible and type‑safe—one of TypeScript’s greatest strengths.