TypeScript Patterns
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 interface
s 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 extend
ing 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.