Open for Extension, Closed for Modification
The Open/Closed Principle says: code should be open for extension — new behavior can be added — but closed for modification — existing, working, tested code is not edited for it.
Every edit to working code is a chance to break it. When adding a feature means only adding new code, the old code stays proven.
TypeScript's tool for this is the interface: a function that depends on an interface never needs to change when a new class implements it.
The switch That Never Stops Growing
This area calculator uses a switch on the shape kind. It works — until the next shape arrives. Triangle? Edit the switch. Pentagon? Edit it again. The function is never finished, and every edit risks the shapes that already worked.
A switch on the shape kind
totalArea must know every kind of shape. Adding one means modifying this tested function — the exact thing the principle warns against.
Output: TypeScript file and captured output
interface ShapeData {
kind: string
width: number
height: number
radius: number
}
// totalArea must know every kind — and grows with each new one.
function totalArea(shapes: ShapeData[]): number {
let total = 0
for (const s of shapes) {
switch (s.kind) {
case "circle":
total += Math.PI * s.radius * s.radius
break
case "rectangle":
total += s.width * s.height
break
}
}
return total
}
const shapes: ShapeData[] = [
{ kind: "circle", width: 0, height: 0, radius: 2 },
{ kind: "rectangle", width: 3, height: 4, radius: 0 },
]
console.log("total area:", totalArea(shapes).toFixed(2))
Extend Through an Interface
The fix: each shape knows its own area. The Shape interface asks for one method, area(), and totalArea just adds up whatever it receives — it no longer cares what the shapes are.
Now watch the principle at work: a brand-new Triangle plugs in, and totalArea is not touched. Extension without modification.
Each shape computes its own area
totalArea depends only on the Shape interface. The switch is gone — and with it, the need to ever edit this function again.
Output: TypeScript file and captured output
interface Shape {
area(): number
}
class Circle implements Shape {
radius: number
constructor(radius: number) {
this.radius = radius
}
area(): number {
return Math.PI * this.radius * this.radius
}
}
class Rectangle implements Shape {
width: number
height: number
constructor(width: number, height: number) {
this.width = width
this.height = height
}
area(): number {
return this.width * this.height
}
}
// totalArea depends only on the interface — no switch, nothing to edit.
function totalArea(shapes: Shape[]): number {
let total = 0
for (const s of shapes) {
total += s.area()
}
return total
}
const shapes: Shape[] = [new Circle(2), new Rectangle(3, 4)]
console.log("total area:", totalArea(shapes).toFixed(2))
Add a new shape, touch nothing
Triangle is pure new code: a class and its area() method. totalArea, Circle and Rectangle are exactly as they were — open for extension, closed for modification.
Output: TypeScript file and captured output
interface Shape {
area(): number
}
class Circle implements Shape {
radius: number
constructor(radius: number) {
this.radius = radius
}
area(): number {
return Math.PI * this.radius * this.radius
}
}
class Rectangle implements Shape {
width: number
height: number
constructor(width: number, height: number) {
this.width = width
this.height = height
}
area(): number {
return this.width * this.height
}
}
// Triangle is NEW code — everything above is untouched.
class Triangle implements Shape {
base: number
height: number
constructor(base: number, height: number) {
this.base = base
this.height = height
}
area(): number {
return (this.base * this.height) / 2
}
}
function totalArea(shapes: Shape[]): number {
let total = 0
for (const s of shapes) {
total += s.area()
}
return total
}
const shapes: Shape[] = [new Circle(2), new Rectangle(3, 4), new Triangle(6, 5)]
console.log("total area:", totalArea(shapes).toFixed(2))