Abstract classes and implementing interfaces
Classes relate to each other in two ways: a class can implement an interface (promising to provide a shape), and a class can extend another, including an abstract base (inheriting and specialising). This lesson is both, and — as with the design-patterns module later — the honest guidance that in TypeScript you should prefer interfaces and composition to deep inheritance.
implements — a class fulfils an interface
An interface describes a shape; a class can promise to provide that shape with implements:
interface Repository<T> {
save(item: T): void;
findById(id: string): T | undefined;
}
class InMemoryRepository<T extends { id: string }> implements Repository<T> {
private items = new Map<string, T>();
save(item: T): void {
this.items.set(item.id, item);
}
findById(id: string): T | undefined {
return this.items.get(id);
}
}
implements Repository<T> tells TypeScript "this class provides everything Repository requires" — and
the compiler checks it. Miss a method, or type one wrongly, and you get an error at the class:
class Broken implements Repository<User> {
save(item: User): void {}
// error: Class 'Broken' incorrectly implements interface 'Repository<User>'.
// Property 'findById' is missing
}
implements is the contract mechanism: the interface defines what is required, and the class is checked
against it. This is how you write code against an interface (function use(repo: Repository<User>)) and
swap in any class that implements it — the polymorphism the interfaces module described, now enforced on
the class side. A class can implement several interfaces (implements A, B), promising all their
shapes.
Note: implements does not add anything to the class — it only checks it. The class must still
provide every member itself; the interface contributes no implementation. This is different from
extends.
extends — inheriting from a base class
extends makes one class inherit another's properties and methods, and specialise them:
class Animal {
constructor(public name: string) {}
move(): string {
return `${this.name} moves`;
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // call the base constructor first
}
move(): string {
return `${super.move()} on four legs`; // extend the base method
}
bark(): string {
return "Woof";
}
}
const d = new Dog("Bruno", "Labrador");
d.move(); // "Bruno moves on four legs"
d.bark(); // "Woof" — Dog's own method
d.name; // "Bruno" — inherited from Animal
Dog extends Animal inherits name and move, adds breed and bark, and overrides move (calling
super.move() to extend rather than replace). super(name) calls the base constructor and must come
first in a subclass constructor. Unlike some languages, TypeScript does not require an override keyword
by default (though the noImplicitOverride option adds one, which is worth enabling — it catches you
overriding a method that no longer exists in the base).
Abstract classes — a base that cannot be instantiated
An abstract class is a base class that provides some implementation but leaves some methods for subclasses to fill in — and it cannot be instantiated directly:
abstract class Shape {
abstract area(): number; // no body — subclasses MUST implement it
describe(): string { // a concrete method, shared by all shapes
return `This shape has area ${this.area()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area(): number { // must implement the abstract method
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(10);
c.describe(); // "This shape has area 314.159..."
const s = new Shape(); // error: Cannot create an instance of an abstract class
abstract class Shape declares an abstract area() (no body — every subclass must provide one) and a
concrete describe() (shared logic that uses the abstract method). You cannot do new Shape() — it is
incomplete. A subclass must implement every abstract method or it, too, is abstract. This is the tool for
"a family of things that share behaviour but each does one part differently" — the base provides the
common part, subclasses provide the specific part. It sits between an interface (all contract, no
implementation) and a normal class (all implementation).
abstract versus interface — which to reach for
They overlap, and the choice matters:
- An
interface— pure contract, no implementation, and a class can implement many. Prefer it when you only need to describe a shape that classes must provide, and especially when a class needs to satisfy several contracts. Interfaces are also erased (zero runtime cost) and work with plain objects, not just classes. - An
abstract class— when you have shared implementation to provide to all subclasses (thedescribe()above), not just a contract. A class can extend only one base, so abstract classes are for a genuine single-inheritance "is-a" hierarchy with shared code.
The rule of thumb: reach for an interface unless you have concrete shared code to inherit — then an
abstract class earns its place. And the deeper guidance, which the design-patterns module reinforces:
prefer interfaces and composition over deep inheritance. A tall inheritance chain (A extends B extends C extends D) becomes rigid and hard to follow; a change high up ripples unpredictably. Most
"reuse" is better achieved by composing small pieces (a class that holds a helper) or programming to
interfaces, than by inheriting. Use inheritance for a genuine, shallow "is-a" specialisation with shared
code; reach for interfaces and composition otherwise. Junior developers over-use inheritance; experienced
ones use it sparingly and deliberately.
Check your work
What implements does. Declares that a class provides an interface's shape, and the compiler checks
it — a missing or mistyped member errors at the class.
Does implements add implementation? No — it only checks; the class must provide every member
itself. A class can implement several interfaces.
What extends does. Inherits a base class's properties and methods, allowing specialisation;
super(...) calls the base constructor (first), super.method() calls the base method.
What noImplicitOverride adds. Requires an override keyword, catching an override of a method that
no longer exists in the base.
What an abstract class is. A base that provides some implementation and leaves abstract methods for
subclasses; it cannot be instantiated directly.
abstract versus interface. Interface for a pure contract (implement many, erased, works with
objects); abstract class when you have shared implementation to inherit (single inheritance).
The guidance on inheritance. Prefer interfaces and composition; use inheritance for a genuine, shallow "is-a" with shared code — deep hierarchies become rigid.
Practice
- Write an interface and a class that
implementsit. Remove a required method and read the error. - Implement two interfaces with one class.
- Write a base class and a subclass with
extends. Override a method, callingsuper.method()inside. Confirmsuper(...)must come first. - Enable
noImplicitOverride, override a method withoutoverride, and read the error. - Write an
abstract classwith an abstract method and a concrete method. Implement it in a subclass. Try tonewthe abstract class and read the error. - Decide, for a "family of related things", whether an interface, an abstract class, or composition fits best, and justify.
- Take a design you would instinctively model with inheritance and reconsider whether interfaces or composition would be clearer.
Official documentation
- TypeScript — implements clauses — Classes fulfilling interfaces.
- TypeScript — extends and inheritance — And
super. - TypeScript — Abstract Classes and Members — Abstract bases.
Next: modules, imports and exports.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship