Small Interfaces
The Interface Segregation Principle says: no type should be forced to implement methods it does not need. Instead of one fat interface with many methods, make several small ones — each type implements only what is true for it.
TypeScript makes small interfaces cheap: types match an interface by shape, so a class satisfies a one-method interface just by having that method. You can define a tiny interface for exactly what your function needs, and every value that fits it already qualifies.
The Fat Interface
A Worker interface with work() and eat() looks harmless — until a robot applies for the job. The robot works fine, but it must implement eat() too, with... something. Nonsense answers like this are the signature of a fat interface.
The robot forced to eat
The robot implements eat() only because the interface demands it. The lunch break prints an answer that should never exist — the class was forced to lie.
Output: TypeScript file and captured output
// One fat interface — every worker MUST also eat.
interface Worker {
work(): string
eat(): string
}
class Human implements Worker {
work(): string {
return "human: working"
}
eat(): string {
return "human: eating lunch"
}
}
class Robot implements Worker {
work(): string {
return "robot: working"
}
// Forced by the interface — robots do not eat.
eat(): string {
return "robot: ERROR, cannot eat"
}
}
const team: Worker[] = [new Human(), new Robot()]
for (const w of team) {
console.log(w.work())
}
for (const w of team) {
console.log(w.eat())
}
Split the Interface
Split the fat interface into Worker and Eater. The human implements both; the robot implements only Worker. The work shift takes workers, the lunch break takes eaters — the robot is simply never invited to lunch, and no method lies.
Each type implements only its truth
Two small interfaces instead of one fat one. The robot no longer has an eat() method at all — the type checker keeps it out of the lunch break automatically.
Output: TypeScript file and captured output
// Two small interfaces — each class implements only its truth.
interface Worker {
work(): string
}
interface Eater {
eat(): string
}
class Human implements Worker, Eater {
work(): string {
return "human: working"
}
eat(): string {
return "human: eating lunch"
}
}
class Robot implements Worker {
work(): string {
return "robot: working"
}
}
function workShift(team: Worker[]): void {
for (const w of team) {
console.log(w.work())
}
}
function lunchBreak(guests: Eater[]): void {
for (const e of guests) {
console.log(e.eat())
}
}
workShift([new Human(), new Robot()])
lunchBreak([new Human()])
Small Pieces Compose
Small interfaces are not a limitation — they combine. When some function really needs both abilities, it composes them into one interface on the spot: Worker plus Eater.
That is the pattern to aim for: define small interfaces, and let whoever needs more glue them together with extends.
Compose when you need both
WorkingEater is built from the two small interfaces. The full-day shift needs both abilities, so only the human qualifies — said once, in the type system.
Output: TypeScript file and captured output
interface Worker {
work(): string
}
interface Eater {
eat(): string
}
// Small interfaces compose into bigger ones when needed.
interface WorkingEater extends Worker, Eater {}
class Human implements WorkingEater {
work(): string {
return "human: working"
}
eat(): string {
return "human: eating lunch"
}
}
function fullDay(we: WorkingEater): void {
console.log(we.work())
console.log(we.eat())
console.log(we.work())
}
fullDay(new Human())