The Power of Inheritance and Interfaces for Building Object-Oriented Systems in TypeScript

2024-07-27

  • In object-oriented programming, inheritance allows you to create new classes (subclasses) that inherit properties and methods from existing classes (superclasses).
  • The extends keyword establishes this relationship.
  • A subclass can access and use the inherited members (properties and methods) of the superclass.
  • It's like a child inheriting traits from a parent.

Example:

class Vehicle {
  color: string;
  constructor(color: string) {
    this.color = color;
  }
  move() {
    console.log('The vehicle is moving.');
  }
}

class Car extends Vehicle { // Car inherits from Vehicle
  wheels: number;
  constructor(color: string, wheels: number) {
    super(color); // Call the superclass constructor to initialize color
    this.wheels = wheels;
  }
  startEngine() {
    console.log('The car engine started.');
  }
}

const myCar = new Car('red', 4);
myCar.move(); // Inherited from Vehicle
myCar.startEngine(); // Specific to Car

Interfaces (implements):

  • Interfaces define contracts that classes must adhere to. They specify the structure (properties and methods) that a class must implement, but they don't provide the actual implementation.
  • The implements keyword is used to declare that a class implements an interface.
  • This ensures that the class provides the required functionality defined in the interface.
  • It's like a blueprint or a set of rules that a class must follow.
interface Flyable {
  fly(): void;
}

class Bird implements Flyable { // Bird implements the Flyable interface
  fly() {
    console.log('The bird is flying.');
  }
}

class Airplane implements Flyable { // Airplane also implements Flyable
  fly() {
    console.log('The airplane is flying.');
  }
}

const sparrow = new Bird();
sparrow.fly();

const boeing = new Airplane();
boeing.fly();

Key Differences:

  • extends creates a parent-child (is-a) relationship, while implements defines a contractual obligation (can-do).
  • A class can only inherit from one superclass, but it can implement multiple interfaces.
  • Inherited members can be overridden in subclasses, while interfaces enforce a mandatory implementation.

Choosing between extends and implements:

  • Use extends when you have a class hierarchy and want to specialize subclasses with additional properties and methods.
  • Use implements when you want a class to provide functionality defined in an interface, regardless of its inheritance structure.



// Base class (superclass)
class Vehicle {
  color: string;

  constructor(color: string) {
    this.color = color;
  }

  move() {
    console.log('The vehicle is moving.');
  }
}

// Subclass (derived class) inheriting from Vehicle
class Car extends Vehicle {
  wheels: number;

  constructor(color: string, wheels: number) {
    super(color); // Call the superclass constructor to initialize color
    this.wheels = wheels;
  }

  startEngine() {
    console.log('The car engine started.');
  }
}

// Create a Car object
const myCar = new Car('red', 4);

// Access inherited and specific methods
myCar.move(); // Inherited from Vehicle
myCar.startEngine(); // Specific to Car
// Interface defining required functionality (contract)
interface Flyable {
  fly(): void;
}

// Class implementing Flyable interface
class Bird implements Flyable {
  fly() {
    console.log('The bird is flying.');
  }
}

// Another class implementing the same interface
class Airplane implements Flyable {
  fly() {
    console.log('The airplane is flying.');
  }
}

// Create Bird and Airplane objects
const sparrow = new Bird();
sparrow.fly();

const boeing = new Airplane();
boeing.fly();



  1. Composition:

    • Instead of inheriting from a class, you can create a separate object (often called a mixin) that encapsulates common functionality.
    • Your main class can then hold a reference to this mixin and access its methods.
    • This approach provides more flexibility and avoids tight coupling between classes.
    // Mixin containing common functionality
    class VehicleMixin {
      color: string;
    
      constructor(color: string) {
        this.color = color;
      }
    
      move() {
        console.log('The vehicle is moving.');
      }
    }
    
    // Main class with a reference to the mixin
    class Car {
      wheels: number;
      vehicleMixin: VehicleMixin;
    
      constructor(color: string, wheels: number) {
        this.wheels = wheels;
        this.vehicleMixin = new VehicleMixin(color); // Create and assign mixin
      }
    
      startEngine() {
        console.log('The car engine started.');
      }
    }
    
    const myCar = new Car('red', 4);
    myCar.move(); // Accessed from the mixin
    myCar.startEngine(); // Specific to Car
    
  2. Dependency Injection:

    • You can inject dependencies as arguments into your class constructor.
    • These dependencies can provide the desired functionality, similar to inherited methods.
    • This approach promotes loose coupling and easier testing.
    // Interface defining the required functionality
    interface Movable {
      move(): void;
    }
    
    // Class implementing the interface
    class Engine implements Movable {
      move() {
        console.log('The engine is providing power for movement.');
      }
    }
    
    // Main class with a dependency
    class Car {
      wheels: number;
      engine: Movable;
    
      constructor(wheels: number, engine: Movable) {
        this.wheels = wheels;
        this.engine = engine; // Inject engine dependency
      }
    
      startEngine() {
        console.log('The car engine started.');
      }
    
      drive() {
        this.engine.move(); // Use injected dependency
      }
    }
    
    const myEngine = new Engine();
    const myCar = new Car(4, myEngine);
    myCar.startEngine();
    myCar.drive(); // Calls move() from the injected engine
    
  1. Type Assertions:

    • If you're absolutely certain a variable holds an object with the desired structure, you can use type assertions to tell TypeScript to treat it as having specific properties and methods.
    • However, this approach bypasses type checking and can lead to runtime errors if the assertion is wrong. Use it cautiously.
    // Function expecting an object with a "fly" method
    function makeFly(flyer: { fly(): void }) {
      flyer.fly();
    }
    
    const bird = { fly() { console.log('The bird is flying.'); } };
    const airplane = { soar() { console.log('The airplane is soaring.'); } };
    
    makeFly(bird); // Works as expected
    
    // Potential runtime error if airplane doesn't have "fly"
    makeFly(airplane as { fly(): void }); // Type assertion (use with caution)
    
  2. Type Guards:

    • Type guards are functions that help narrow down the type of a variable based on runtime checks.
    • You can use type guards to ensure a variable has the required properties before using them.
    • This provides better type safety compared to type assertions.
    // Function expecting an object with a "fly" method
    function makeFly(flyer: unknown) {
      if (typeof flyer.fly === 'function') {
        flyer.fly();
      } else {
        console.error('Object does not have a fly method.');
      }
    }
    
    const bird = { fly() { console.log('The bird is flying.'); } };
    const airplane = { soar() { console.log('The airplane is soaring.'); } };
    
    makeFly(bird); // Works as expected
    makeFly(airplane); // Catches the error without runtime issues
    

typescript extends implements



Understanding Getters and Setters in TypeScript with Example Code

Getters and SettersIn TypeScript, getters and setters are special methods used to access or modify the values of class properties...


Taming Numbers: How to Ensure Integer Properties in TypeScript

Type Annotation:The most common approach is to use type annotations during class property declaration. Here, you simply specify the type of the property as number...


Mastering the Parts: Importing Components in TypeScript Projects

Before you import something, it needs to be exported from the original file. This makes it available for other files to use...


Alternative Methods for Handling the "value" Property Error in TypeScript

Breakdown:"The property 'value' does not exist on value of type 'HTMLElement'": This error indicates that you're trying to access the value property on an object that is of type HTMLElement...


Defining TypeScript Callback Types: Boosting Code Safety and Readability

A callback is a function that's passed as an argument to another function. The receiving function can then "call back" the passed function at a later point...



typescript extends implements

Understanding TypeScript Constructors, Overloading, and Their Applications

Constructors are special functions in classes that are called when you create a new object of that class. They're responsible for initializing the object's properties (variables) with starting values


Alternative Methods for Setting New Properties on window in TypeScript

Direct Assignment:The most straightforward method is to directly assign a value to the new property:This approach creates a new property named myNewProperty on the window object and assigns the string "Hello


Alternative Methods for Dynamic Property Assignment in TypeScript

Understanding the Concept:In TypeScript, objects are collections of key-value pairs, where keys are property names and values are the corresponding data associated with those properties


Alternative Methods for Type Definitions in Object Literals

Type Definitions in Object LiteralsIn TypeScript, object literals can be annotated with type definitions to provide more precise and informative code


Alternative Methods for Class Type Checking in TypeScript

Class Type Checking in TypeScriptIn TypeScript, class type checking ensures that objects adhere to the defined structure of a class