Lesson 16 of 25
Abstract Classes and Polymorphism
implements versus extends, abstract classes that can't be instantiated directly, and how TypeScript checks that a class actually satisfies the interfaces it claims to.
Classes so far have covered fields and visibility. This lesson covers sharing a contract across several classes two different ways — implementing an interface's shape, or extending a base class's actual behaviour — and a class that deliberately can't be instantiated on its own.
implements: satisfying a shape
A class can declare that it satisfies an interface with implements. This
is checked at compile time and contributes no runtime behaviour at all — it
does not "inherit" anything, it just verifies the shape matches:
interface Playable {
play(): void;
pause(): void;
}
class VideoPlayer implements Playable {
play() {
console.log("Playing video");
}
pause() {
console.log("Paused video");
}
}Leave out pause() entirely, and it's a compile error, right at the class
declaration — the same feedback you'd get for a missing property on an
object literal typed as Playable:
class BrokenPlayer implements Playable {
play() {
console.log("Playing");
}
}
// Class 'BrokenPlayer' incorrectly implements interface 'Playable'.
// Property 'pause' is missing in type 'BrokenPlayer'.A class can implement more than one interface at once:
interface Loggable {
log(): void;
}
class VideoPlayer implements Playable, Loggable {
play() { /* ... */ }
pause() { /* ... */ }
log() { /* ... */ }
}extends: inheriting real behaviour
extends is different in kind, not just in name — a subclass actually
inherits a base class's implementation, and can call it via super:
class Animal {
constructor(public name: string) {}
describe(): string {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
describe(): string {
return `${super.describe()} Specifically, a bark.`;
}
}
const dog = new Dog("Rex");
console.log(dog.describe()); // "Rex makes a sound. Specifically, a bark."super.describe() calls the base class's version before extending it —
genuine reuse of implementation, which implements never provides.
Abstract classes
An abstract class sits between "a plain base class, fully usable on its
own" and "an interface, with no implementation at all." It can define real,
shared implementation, declare members that every subclass must implement
without providing them itself, and — critically — cannot be instantiated
directly:
abstract class Shape {
abstract area(): number; // no implementation — every subclass must provide one
describe(): string {
return `This shape has an area of ${this.area()}`; // shared, real implementation
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
new Shape();
// Cannot create an instance of an abstract class.
const circle = new Circle(3);
console.log(circle.describe()); // uses Shape's real describe(), calls Circle's area()If Circle forgot to implement area(), that would be a compile error at
the Circle declaration — the same guarantee implements gives you, but
combined with actual shared behaviour (describe()) that every subclass
gets for free.
Polymorphism: one interface, many shapes
The payoff of both mechanisms is the same: code written against the shared type doesn't need to know which concrete class it's actually holding.
class Square extends Shape {
constructor(private side: number) {
super();
}
area(): number {
return this.side ** 2;
}
}
function printArea(shape: Shape) {
console.log(shape.describe()); // works identically for any Shape subclass
}
printArea(new Circle(3));
printArea(new Square(4));printArea never checks which specific shape it received — every subclass
of Shape satisfies the same contract, and calling .describe() (which
itself calls .area()) dispatches to whichever concrete implementation the
actual object has. This is polymorphism: the same call, correct behaviour
per concrete type, decided at runtime by which class was actually
instantiated.
Try it yourself
What to remember
- implements checks a class satisfies an interface's shape at compile time; extends inherits real, reusable implementation from a base class.
- A class can implement multiple interfaces at once, but only extend one base class.
- An abstract class can mix real shared implementation with abstract members every subclass must provide, and cannot itself be instantiated with
new. - Polymorphism means code written against a shared base type works correctly for any subclass, dispatching to the right concrete implementation at runtime.
Check yourself
4 questions · pass 3/4 to unlock Conditional Types
1.What is the core difference between a class using implements and one using extends?
2.What happens if you try to instantiate an abstract class directly with new?
3.A subclass extends an abstract class but doesn't implement one of its abstract methods. What happens?
4.Can a single class implement more than one interface at once?
4 left to answer