TypeScript Classes and Inherintance

TypeScript OOP concepts.


OOP in TypeScript

Working with objects is also possible in JavaScript as we already know, however, TypeScript offers some utilities we don't have in plain JavaScript.

Objects are instances of classes. A class defines a reusable component that defines something we need to work with.

Some examples

Let's now give some real-world examples. Let's then suppose we need to define a player for a video-game.

A player could have some properties such as its total health, and its speed. We'd also need to define some behaviors, for example, how it moves and attacks.

Let's translate this into code.

class Player {
  health: number;
  speed: number;

  constructor(health: number, speed: number) {
    this.health = health;
    this.speed = speed;
  }

  move(): void {}

  attack(): void {}
}

const player = new Player(100, 8);

The Player class consists of two properties, a Constructor, and two methods.

  • Property: variable bound to the class.
  • Constructor: a subroutine called once we create an instance of the class.
  • Methods: functions bound to the class.

Inherintance

Using classes is easy enough, however, we should totally avoid repeating ourselves.

Let's suppose we have to create two objects, Dog and Snake.

They're both animals, they share some properties such as name and age, and they both sleep.

Nonetheless, while a dog has legs and walks, a snake doesn't have any leg and crawls.

Let's then create a single class Animal, with the members in common.

class Animal {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  sleep(): void {}
}

We can take advantage of Inherintance to have avoid repeating the same members across sub-classes (Dog, Snake).

Using Inherintance in TypeScript requires the use of the extends keyword.

class Dog extends Animal {
  legs: number = 4;

  walk(): void {}
}

class Snake extends Animal {
  crawl(): void {}
}

const dog = new Dog('Alvin', 4);
dog.name; // Alvin
dog.age; // 4
dog.legs; // 4
dog.sleep();
dog.walk();

const snake = new Snake('Claire', 8);
dog.name; // Claire
dog.age; // 8
dog.sleep();
dog.crawl();

Abstract Classes / Methods

Sometimes you don't want to instantiate a class directly and rather using it as a base for other sub-classes. To do so you can use Abstract Classes.

Similar reasoning goes behind Abstract Methods: methods that don't contain implementation details, and must be implemented in derived classes.

abstract class Animal {
  abstract sleep(): void;
}

class Dog extends Animal {
  sleep(): void {
    console.log('Zzzzz zzz zzz');
  }
}

Static Properties

Static Properties are only visible on the class itself, rather than on their instances.

For example:

class Vector {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  static sum(vector1: Vector, vector2: Vector): Vector {
    const x = vector1.x + vector2.x;
    const y = vector1.y + vector2.y;

    return new Vector(x, y);
  }
}

const vector1 = new Vector(2, 4);
const vector2 = new Vector(4, 0);

const sumVector = Vector.sum(vector1, vector2);
sumVector.x; // 6
sumVector.y; // 4

Access Modifiers

By default, in JavaScript, every property of a class is public.

Creating private members is technically possible in JavaScript, yet it doesn't exist a specific syntax to do that. Making use of Access Modifiers in JavaScript makes the code harder to read.

In TypeScript, we can use a bunch of keywords to specify the access level of members:

  • public: every member is public by default, you can still make it explicit. Public means you can access this property from outside the class as well.
  • private: makes the member inaccessible from the outside. Keep in mind that a private member is inaccessible from any sub-class when using Inheritance.
  • protected: similar functionality to private, though you can access protected members from within any sub-class.
class Animal {
  public name: string;
  private age: number;
  protected legs: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Dog extends Animal {
  getAge() {
    console.log(this.age);
    // Error, age is private! Only accessible in Animal
  }
}

const animal = new Animal('Alvin', 4);
animal.name; // Alvin
animal.age; // Error - private
animal.legs; // Error - protected

const dog = new Dog('Claire', 8);
animal.name; // Alvin
animal.age; // Error - private
animal.legs; // Error - protected

Readonly modifier

It's also possible to create a read-only member by using the readonly keyword.

class Animal {
  public readonly name: string;
  private age: number;
  protected legs: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const animal = new Animal('Alvin', 4);
animal.name = 'Kevin'; // Error, name is read-only!

Accessors

TypeScript also supports Accessors. By using them, you have higher control over the way each member is accessed or edited from the outside.

For instance:

class Animal {
  private name: string;
  private age: number;
  private legs: number;

  get getName() {
    return this.name;
  }

  set setName(name: string) {
    if (!name) throw new Error("The name can't be an empty string!");

    this.name = name;
  }

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const animal = new Animal('Alvin', 4);
animal.getName; // Alvin
animal.setName(''); // Error! The name can't be an empty string!
animal.setName('Steve');
anima.getName; // Steve

Using Interfaces

In the previous post, I talked about interfaces and how to use them to create structured patterns. You can do the same for classes by using the implements keyword.

For example:

interface AnimalInterface {
  name: string;
  age: number;
  sleep: () => void;
}

// This will throw an error
// Type 'Animal' is missing the following properties from type 'AnimalInterface': name, age, sleep
class Animal implements AnimalInterface {}

// This class instead implements the interface correctly
class Animal implements AnimalInterface {
  name: string = '';
  age: number = 0;

  sleep(): void {}
}