Skip to content

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.