Full-stack Web Technologies

CHAPTER 2
Typescript Classes

Using prototypical inheritance is both cumbersome and very different from other languages. ES6 decided to add syntax to make it easier, and Typescript took that further. However, the mechanism behind this syntax is still based on prototypes, it is just a compiler translation.

In Typescript, classes are defined in this way:

class Person {
  name: string;

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

  sayHi() {
    console.log(`Hi, I'm ${this.name}`);
  }
}

The class keyword is used to define a class, and inside the braces we list the fields, a constructor and any methods we need.

Creating instances of the Person class is still done with new:

let person = new Person("Alice Bebop");

Getters and Setters

Getters and Setters simulate the existence of fields that in reality end up calling methods. This allows the programmer to catch accesses to old fields and reconvert them to methods instead, allowing for more control over encapsulation.

To define a getter prefix it with get:

class Person {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

The getter fullName is used as if it was a normal field, giving the appearance that the field in fact exists:

let p = new Person("Bob", "Parr");
console.log(p.fullName);

The converse of reading a field is assignining to a field. Assignment also can be turned into a method call defining a setter:

class Person {
  // ...
  set fullName(fn: string) {
    const [first, last] = fn.split(' ');
    if (first) this.firstName = first;
    if (last) this.lastName = last;
  }
}

Now we can even set the fullName as if it was a field:

let p = new Person("Bob", "Parr");
p.fullName = "Robert Parr";

Inheritance and interfaces

To express inheritance (something that prototypes make difficult) now we only have to use extends:

class Superhero extends Person {
  hero: string;

  constructor(name: string, hero: string) {
    super(name);
    this.hero = hero;
  }

  breakThroughWall() {
    console.log(`Look! ${this.hero} broke through a wall!`);
  }
}

The call to super() is required, even if the base class has no parameters, and it must be the first instruction in the constructor.

Typescript also allows to use implements when a class matches a more general type. If we have an interface that describes a type:

interface HasName {
  name: string;
}

Then a class can express that it satisfies that interface with implements:

class Person implements HasName {
  name: string;

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

  //...
}

instanceof

To determine if an object is an instance of a class, a special mechanism is needed and Javascript has a new operator called instanceof which will check if an object belongs to a class:

class C {}
let x = new C();
x instanceof C      // true
x instanceof Array  // false
x instanceof Error  // false
x instanceof Object // true

class D extends C {}
let y = new D();
y instanceof C      // true
y instanceof D      // true
y instanceof String // false
y instanceof Object // true

Private fields

Typescript allows to mark fields private (also protected):

class Watch {
  private hour: number;
  private minute: number;
  private second: number;

  constructor(hour: number, minute: number, second: number) {
    this.hour = hour;
    this.minute = minute;
    this.second = second;
  }
}

Private fields cannot be accessed from code outside of the class itself. This is useful to protect their values from logic mistakes introduced in the rest of the program.

However, this type of privacy is just checked by Typescript, and therefore, if you iterate over the properties of the object dynamically (with for-in), then private fields will show up.

Hard privacy

Javascript itself has introduced recently truly private fields at the level of the runtime, which really cannot be accessed by outside code.

Private fields start with # (they have "hash names"), and it is a compile-time error to access them outside of the class:

class Watch {
  #hour: number;
  #minute: number;
  #second: number;

  Watch() {
    this.#hour = 0;
    this.#minute = 0;
    this.#second = 0;
  }
}

let w = new Watch();
w.#hour = -17; // Syntax error

Abstract classes

Abstract classes define a base class which cannot possibly implement certain methods because they are really dependent on the specific derived classes.

Using abstract we can both mark classes as abstract whenever they have at least one method which is abstract:

abstract class Fruit {
  constructor(public weight: number) {}

  eat() {
    console.log("You ate a fruit");
  }

  abstract peel(): void;
}

class Apple extends Fruit {
  constructor(weight: number) {
    super(weight);
  }

  peel() {
    console.log("You are peeling an apple");
  }
}