typescript interfaces classes inheritance

Typescript: Working with Classes and Inheritance using an analogy with Family

Recent developments of javascript have support for classes enabling developers who love Object Oriented Programming to have a similar experience using javascript to work with classes. Classes were introduced in javascript since ES6/ES2015.

In javascript, classes are still functions but special ones that serve as a template for holding data and methods that modify the data(encapsulation). They are a great way to represent your data and actions that you perform on them. One thing to note about classes in javascript when it comes to hoisting is that they cannot be called before defining them unlike functions which can be called before their definition as the values of classes are not initialized during hoisting.

Example:

// calling class before definition
const family1 = new Family() // this will give a referece error
// class definition
class Family {}

With functions on the other hand:

// calling function before definition
const family1 = family() // this will throw no error

// function definition
function family() {}

Let’s take a look at an example of a class in javascript.

class Family {
  name;
  constructor(name) {
    this.name = name;
  }
}

class Family1 extends Family {
  constructor(name) {
    super(name);
  }

  get description() {
    return `${this.name}, is the name of this family`;
  }
}

You can read more on javascript on MDN web docs.

Now, this is all good and we are able to do a lot so far with just javascript but if you do have an OOP background you will be thinking no this is still not enough as you might want more than just representing your data but other features like support for a type system in your classes and being able to use parameter modifiers for your classes are all missing here. Even though this a fundamental issue with javascript when it comes to type support for instance, typescript can help us to solve some of these issues by allowing us to write better typed classes and use modifiers where we need to. Typescript will of course not capture any runtime errors but during the development of our application, it will allow us to catch errors before our code is compiled and that is a big plus.

Working with Interfaces

In the examples described below, we try to show classes and inheritance in typescript with a family analogy.

To represent our family, we’ll use a typescript Interface, learn more about typescript interfaces. Here, we represent the name of the family and other properties like the total number of members in the family and their wealth.

interface FamilyDescription {
  // by marking the name as readonly, we prevent it from being changed later
  readonly name: string;
  members: {
    total: number;
    wealth: number;
  }
}

Implementing Interfaces

We can then construct a new family that implements this interface, basically, this will allow us to ensure that this family has the properties in the interface.

class Family implements FamilyDescription {
  constructor(public readonly name: string, public members: {total: number, wealth:number}) {
    // capitalize first name
    this.name = name.charAt(0).toUpperCase() + name.slice(1)
  }
  // since there's no setter, by default this property will be readonly
  get wealth(): string {
    return `${this.name} family's wealth is USD${this.members.wealth}`
  }
  get description(): string {
    return `${this.name} family has ${this.members.total} members.`
  }
}

const george = new Family('george',{total: 20,wealth: 200})

console.log(george.wealth)
// OUTPUT: "George family's wealth is USD200"

We first declare our class and use the implements keyword to tell typescript that we want the class to have the properties of the interface. It does not affect the classes methods or properties and how they are implemented.

Using the public modifies in the constructor, we can declare the properties in the constructor without having to specify with this keyword, like how we’d normally do with this.name = name for example. We use the readonly property to ensure that the name cannot be set on the class after initializing the class.

We then decare getters description and wealth which gives us information about the class. Without adding a setter for these methods, typescript will by default set them as readonly properties on the class.

If you want to use private and protected property modifiers on the Family class properties, you would have to implement them separately from the interface. Currently (as of version 4.7.4), typescript does not allow you to set modifiers on interfaces so any property in an interface will be public.

Extending the class

There are some common properties like wealth and description that we might reuse if we wanted to extend the family or have subclasses of the family like a Nuclear family or an Extended family.

Abstract classes allow us to do this by providing a template as to the behaviour of our classes and extending them on any class that depends on the same behaviour.

interface ExtendedFamilyDescription {
    _grandparents: number;
}

abstract class Family implements FamilyDescription {
  constructor(public readonly name: string, public members: {total: number, wealth:number}) {
    // capitalize first name
    this.name = name.charAt(0).toUpperCase() + name.slice(1)
  }
  // since there's no setter, by default this property will be readonly
  get wealth(): string {
    return `${this.name} family's wealth is USD${this.members.wealth}`
  }
  get description(): string {
    return `${this.name} family has ${this.members.total} members.`
  }
}

class NuclearFamily extends Family{}

class ExtendedFamily extends Family {
  public _grandparents = 0
  get grandparents() {
    return this._grandparents
  }
  set grandparents(value) {
    this._grandparents = value
  }
  // we overwrite the description from the Family class here
  get description(): string {
    return `${this.name} family has ${this.members.total} members and ${this._grandparents} grandparents!`
  }
}

const george = new NuclearFamily('george',{total: 20,wealth: 200})
console.log(george.wealth) // OUTPUT: George family's wealth is USD200
const evans = new ExtendedFamily('evans',{total: 20,wealth: 200})
console.log(evans.description) // OUTPUT: Evans family has 20 members and 0 grandparents!
evans.grandparents = 25
console.log(evans.description) // OUTPUT: Evans family has 20 members and 25 grandparents!

Since we don’t need to update the behaviour of our Nuclear Family, we don’t also need to add the constructor. It will just inherit the behavior of the Family abstract class.

Abstract classes cannot be instantiated and can only be extended by other classes. Trying to initialize the Family class with: const evans = new Family('evans',{total: 2, wealth: 300}) will fail. In the initial instance where it was not an abstract class though, it will work alright as in the example with george’s family.

Read more about typescript classes on the official documentation.