Inheritance

Beginner · 10 min read · ▶ live playground · ✦ checkpoint

A cargo bike and a delivery van both need an ID label and a trip estimate. You copy those methods into two classes, then fix a rounding bug in only one copy. The classes look independent, but their shared behavior is already drifting apart.

Inheritance is a relationship where one class receives state and behavior from another class, then may add or change behavior of its own. The class being inherited from is the parent class, also called the base class. The class that inherits is the child class, also called the derived class.

In TypeScript, extends declares that relationship. It can remove honest duplication, but it also ties the child to the parent's design.

Put shared behavior in the parent

Before drawing an inheritance arrow, use the is-a test: read “Child is a Parent” aloud and decide whether it is true in the problem, not just convenient in the code. A cargo bike is a delivery vehicle. A delivery van is also a delivery vehicle. Both can use the vehicle's label, while either child may add details that only it knows.

class diagram

Delivery vehicles share a base class

Delivery vehicles share a base class
  1. 1DeliveryVehicle owns the ID label and the default travel estimate shared by its children.
  2. 2CargoBike extends DeliveryVehicle, so it receives the vehicle's public behavior.
  3. 3CargoBike overrides travelMinutes() because city-bike travel has a different calculation.
  4. 4DeliveryVan extends the same parent and keeps the parent's default travel estimate.

The solid hollow-triangle arrows point toward the more general type. Read them from child to parent: CargoBike extends DeliveryVehicle; DeliveryVan extends DeliveryVehicle.

The parent is not a storage cupboard that children raid for code. It represents a real category in the model. That meaning is what makes a DeliveryVehicle reference useful later: every child honors the vehicle's public operations.

Extend the class, then override one decision

An inherited method is a parent method available on a child even though the child does not declare it again. Overriding means a child supplies a new implementation of an inherited method with the same callable shape.

class DeliveryVehicle {
  private vehicleId: string;
 
  public constructor(vehicleId: string) {
    this.vehicleId = vehicleId;
  }
 
  public label(): string {
    return `Vehicle ${this.vehicleId}`;
  }
 
  public travelMinutes(distanceKm: number): number {
    return Math.ceil((distanceKm / 30) * 60);
  }
}
 
class CargoBike extends DeliveryVehicle {
  private loadingDelay = 4;
 
  public override travelMinutes(distanceKm: number): number {
    const ridingMinutes = Math.ceil((distanceKm / 15) * 60);
    return ridingMinutes + this.loadingDelay;
  }
 
  public secureBox(): string {
    return `${this.label()} cargo box locked`;
  }
}
 
class DeliveryVan extends DeliveryVehicle {
  private palletSlots = 6;
 
  public loadPallets(count: number): string {
    if (count > this.palletSlots) {
      return `${this.label()} cannot carry ${count} pallets`;
    }
 
    return `${this.label()} loaded ${count} pallets`;
  }
}
 
const bike = new CargoBike("BIKE-12");
const van = new DeliveryVan("VAN-07");
 
console.log(`${bike.label()}: ${bike.travelMinutes(6)} minutes`);
console.log(bike.secureBox());
console.log(`${van.label()}: ${van.travelMinutes(6)} minutes`);
console.log(van.loadPallets(4));

Vehicle BIKE-12: 28 minutes Vehicle BIKE-12 cargo box locked Vehicle VAN-07: 12 minutes Vehicle VAN-07 loaded 4 pallets

Neither child redeclares label(). Both inherit it from DeliveryVehicle, so the ID format lives in one place. DeliveryVan also inherits the default travelMinutes() calculation: six kilometres at 30 kilometres per hour takes 12 minutes.

CargoBike needs another rule. Its method has the same name, parameter type, and returned type as the parent method, but it estimates a slower speed and adds four loading minutes. The override keyword is a TypeScript check that says, “I intend to replace a parent method.” If the parent method is renamed, TypeScript can flag the child instead of quietly treating its method as unrelated.

The child keeps the parent's boundary

Inheritance does not turn private state into child-owned state. vehicleId remains private to DeliveryVehicle. CargoBike can call the public label() method, but it cannot read this.vehicleId directly.

That preserves the encapsulation boundary from the previous lesson. The parent decides how IDs are stored and displayed. A child receives the public behavior, not permission to bypass the parent's rules.

An override should also preserve the meaning callers expect. Both travel methods still accept a distance and return estimated minutes. If CargoBike.travelMinutes() started returning battery percentage, it would match the return type only by accident and break the promise carried by the method name.

When not to use inheritance

A deep hierarchy is a chain with many parent-to-child levels. Imagine DeliveryVehicle → Bike → ElectricBike → RefrigeratedElectricBike → FestivalRefrigeratedBike. To understand the final class, you must inspect decisions scattered across every level.

Deep trees make the fragile base class problem more likely: a change that looks safe in a parent unexpectedly changes or breaks several children. Even a shallow hierarchy deserves care when children override many parent methods or depend on hidden details of the parent's implementation.

Do not inherit only to reach a handy method. A ReceiptPrinter is not a DeliveryVehicle, even if the vehicle has a label formatter the printer wants. The is-a test fails. In that case, the printer should hold or call a separate formatter instead of pretending to be a vehicle.

Inheritance remains useful when the category is genuine, the shared public contract matters, and the hierarchy stays understandable. The alternative is not duplicated code by default. The composition lesson will show how one object can hold another object that supplies reusable behavior without creating a parent-child claim.

Checkpoint

Answer all three to mark this lesson complete

You can now build an honest family of types and let a child refine one inherited decision. Next, polymorphism lets a caller use the parent type while the object chooses the right override at runtime.

+50 XP on completion