TypeScript Patterns

In notebook:
Work Notes
Created at:
2019-11-23
Updated:
2020-02-06
Tags:
JavaScript Fundamentals

Destructuring with Typescript

My notes from the Udemy course by Stephen Grider. Please watch the course first, then come back for the notes.

You can do destructuring in Typescript, but the type annotation comes after the destructuring.

  
const logWeather = ({ date, weather }: { date: Date, weather: sting}) : void {
  console.log(date);
  console.log(weather);
}

For nested objects:

  const {
  coords: { lat, lng } 
}: { coords: { lat: number; lng: number } } = profile;

Two dimensional arrays

  const carsByMake: string[][] = [['foo', 'bar'], ['baz', 'fro']]

Arrays with different types

  const importantDates: (Date | String)[] = [new Date(), 'foo']

Methods in Interfaces

  interface Vehicle {
  name: string;
  year: Date;
  summary(): string; // a function returning a date
}

The two syntaxes to initialising a class:

The longform:

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

Or, you can use a shortform by adding the accessor type:

  class Vehicle {
  constructor(public color: string) {}
}

In Typescript you have to initialise all class properties:

  class User {
  name: string;
  age: number;
  constructor(name) {
    this.name = name;
    console.log(this.age) // -> this will throw an error
    // you have to initialise this.age as well
  }
}

Limiting the available methods of a class

In the workshop, we wanted to limit the Google Maps methods that are accessible from new "instances" of our class.

  export class CustomMap {
  private googleMap: google.maps.Map;

  constructor() {
    this.googleMap = new google.maps.Map(document.getElementById('map'), {
      zoom: 1, 
      center: { lat: 0, lng: 0}
    })
  }

  addMarker(user: User): void {
     new google.maps.Marker({
      map this.googleMap,
      position: { lat: ..., lng: ...}
     })
  }
}

With the private we instucted Typescript that when you do var mymap = new CustomMap() the, mymap.googleMap will not be accessible (will show an error), only mymap.addMarker() will be accessible.

A generic interface and class setup with interfaces

You can use interfaces to define the arguments that need to be passed to your constructor:

  interface Mappable {
  location: {
    lat: number;
    lng: number;
  }
}

// then in your class:

export class CustomMap {
  //...
  constructor() {
    //...
  }

  addMarker(mappable: Mappable): void {
     new google.maps.Marker({
      map this.googleMap,
      position: {
        lat: mappable.location.lat,
        lng: mappable.location.lng
      }
     })
  }
}

Enums

These are "objects" that store some closely related values.

In normal JavaScript you would do:

  const MatchResult = {
  HomeWin: 'H',
  AwayWin: 'A',
  Draw: 'D'
};

To list the possible values (think Redux action names). In TypeScript it would be:

  const MatchResult {
  HomeWin = 'H',
  AwayWin = 'A',
  Draw = 'D'
};

Code reuse: Inheritance vs composition

There are two different strategies for code reuse when doing OOP. These can be summarised as "is a" relationship (inheritance) or "has a" (composition).

Abstract classes (inheritance)

This used for inheritance type patterns. In TypeScript an abstract class is never initialised on its own, only by extending via other subclasses. The abstract class has some general methods that all subclasses would need and use the same way, but more specific functionalities are implemented by the subclasses. These "specific functionalities" are still of similar domain, e.g. one sublcass would handle parsing one type of data, e.g. sports matches results, while another would parse movie ratings data, but they both subclasses of e.g. CsvFileReader.

To create an abstract class, with some abstarct methods:

  abstract class CsvFileReader {
  data: MatchData[] = [];
  const(public filename: srting) {}

  abstract mapRow(row: string[]): MatchData;
}

Now, any sublcass can (has to) implement mapRow to work with more specific scenarios, e.g. parse diffrent types of data.

Adding Generics

Now you can add TypeScript generics to make CsvFileReader work with any type of data:

  abstract class CsvFileReader<T> {
  data: T[] = [];
  const(public filename: srting) {}

  abstract mapRow(row: string[]): T;
}

And to "use" (extend) CsvFileReader you would do:

  class MatchReader extends CsvFileReader<MatchData> {
  mapRow(row: string[]): MatchData {
    return [
      //...
    ]
  }
}

Interface based approach

Instead of creating a parent (abstract) class, you can start off with the MatchReader (above), 1. specify that it has a reader method, and 2. create an interface (e.g. DataReader) to specify what the reader should take and return. Then, when you instantiate your class, you specify (an argument to the constructor) what that reader should be (e.g. CsvFileReader, or an ApiReader that loads the data from a server). The only requirement is that this reader satisfies the DataReader interface.

  // first, define the interface
// any class/function that implements this
// can be used for composition
interface DataReader {
  read(): void;
  data: string[][];
}

class MatchReader {
  matches: MatchData[] = [];

  reader: DataReader;
  // you pass the constructor the actual
  // reader implementation
  constructor(public reader: DataReader) {}

  // then use it
  load(): void {
    this.reader.read();
    this.reader.data.map(
      (row: string[]): MatchData => {
        return [
          // data transformations
          // ...
        ]
      }
    )
    // then finally store the results in our class
    this.matches = this.reader.data;
  }
}

One thing to note about the above composition pattern is that the reader is a completely independent function/class. The this variable inside it and the this of the class using are not pointing to each other. This is why you need to store the this.reader.data in this.matches variable. There could be more complex pattern where we do link one prototype to another that would be more elegant to use.

Inheritance and composition

Again, inheritance is when you have a class with some basic functionalities that will be used "verbatim" by all other classes. Other classes extend this base class with some extra functionalities but they all point to the same base class and it's methods will be used the same way.

In composition, you specify at the time instantiatation the actual implementation that will use a certain method of your class. So a "base" class would have a reader method, but no implementation of it.

Inherintance drawbacks

The problem with inheritance type relationships that you can easily hit a dead end. The classical example (also used in the workshop) is a house. A house may have walls and windows so you could create a Rectangle base class, that both Wall and Window would extend. The base Rectangle would have a width, height property, and an area() method. The Window in addition would have a toggleOpen method. As soon as you want to add a circular type window, the relations break, because both type of windows would need to have a toggleOpen method, but only one can inherit from Rectangle.

The hierarchial model means that different objects cannot have a mix of different methods, but only what their parents have and what they themselves add. If you decide that a Dog class should inherit from Animal, you cannot later mix it to be a RobotDog that has some methods from Animal (e.g. walk, bark), and also some methods from Robot (drive, rechargeBatteries).

Composition

With composition, you can instead just import (compose into) the actual methods and properties a particular object needs. In this case, you would create a Wall and a Window objects, that have an dimensions property and area() method. The actual values and calculations would come either from Rectangle or Circle obects that you "compose" into your object.

So again, in composition, you do have some predefined properties and methods (interface in a TypeScript environment), but these properties and methods will be added in when you run the constructor function.

General misconception about composition

Favor object composition over class inheritance

from the book Design Pattern, Elements of Reusable Object-Oriented Software is greatly endorsed by the general JavaScript community, but most explanations that you can find online are not true to the original concept presented in the book.

On most blog posts, you will find an explanation that builds up a bigger object from several smaller ones:

  const barker = state => ({
  bark: () => console.log('Woof, I am ' + state.name);
})

const fly = state => ({
  fly: () => {
    console.log('I can fly' + state.name);
    state.posy = state.posy + 100;
  }
})

// create an object
// that has several characteristics
const flyingdog = name => {
  let state = {
    name,
    posy: 0
  }
  return Object.assign(state, fly(state), barker(state));
}

flyingdog('jessie').bark();
// 'Woof, I am jessie'

According to the instructor (Stephen Grider), these presenations miss the point of composition. In his view, the above pattern is still closer to inheritance. We are just copy-pasting in different methods, but they all expect to work on some base properties (e.g. the state.posy value). So most bloglost just describe a different way of building up an object, that look more like composition. Also, the above pattern can be a source of bugs, when combining several objects that have the same methods (say both, barker and fly have a report() method, they one would overwrite the other).

According to Stephen the correct name for the above pattern is Multiple Inherintance.

To be true to the original definition as described in the book, you would have an object, that has a communicate and a move method, but without implementation. Then when you instantiate this object, you provide an implementation of communicate (e.g. bark) and an implementation of move (e.g. fly). You can also pass in different implementations of communicate and move for different scenarios.