RizTech Academy logo
RizTech Academy
Classes, Modules and DecoratorsLesson 2 of 425 min

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 (the describe() 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

  1. Write an interface and a class that implements it. Remove a required method and read the error.
  2. Implement two interfaces with one class.
  3. Write a base class and a subclass with extends. Override a method, calling super.method() inside. Confirm super(...) must come first.
  4. Enable noImplicitOverride, override a method without override, and read the error.
  5. Write an abstract class with an abstract method and a concrete method. Implement it in a subclass. Try to new the abstract class and read the error.
  6. Decide, for a "family of related things", whether an interface, an abstract class, or composition fits best, and justify.
  7. Take a design you would instinctively model with inheritance and reconsider whether interfaces or composition would be clearer.

Official documentation

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