Advanced TypeScript Programming Projects
上QQ阅读APP看书,第一时间看更新

Composing types using mixins

When we first encounter classic OO theory, we come across the idea that classes can be inherited. The idea here is that we can create even more specialized classes from general-purpose classes. One of the more popular examples of this is that we have a vehicle class that contains basic details about a vehicle. We inherit from the vehicle class to make a car class. We then inherit from the car class to make a sports car class. Each layer of inheritance here adds features that aren't present in the class we are inheriting from. 

In general, this is a simple concept for us to work with, but what happens when we want to bring two or more seemingly unrelated things together to make our code? Let's examine a simple example.

It is a common thing with database applications to store whether a record has been deleted without actually deleting the record, and the time that the last update occurred on the record. At first glance, it would seem that we would want to track this information in a person's data entity. Rather than adding this information into every data entity, we might end up creating a base class that includes this information and then inheriting from it:

class ActiveRecord {
Deleted = false;
}
class Person extends ActiveRecord {
constructor(firstName : string, lastName : string) {
this.FirstName = firstName;
this.LastName = lastName;
}

FirstName : string;
LastName : string;
}

The first problem with this approach is that it mixes details about the status of a record with the actual record itself. As we continue further into OO designs over the next few chapters, we will keep reinforcing the idea that mixing items together like this is not a good idea because we are creating classes that have to do more than one thing, which can make them less robust. The other problem with this approach is that, if we wanted to add the date the record was updated, we are either going to have to add the updated date to ActiveRecord, which means that every class that extends ActiveRecord will also get the updated date, or we are going to have to create a new class that adds the updated date and add this into our hierarchy chain, which means that we could not have an updated field without a deleted field.

While inheritance definitely does have its place, recent years have seen the idea of composing objects together to make new objects gain in prominence. The idea behind this approach is that we build discrete elements that do not rely on inheritance chains. If we revisit our person implementation, we will build the same features using a feature called a mixin instead.

The first thing we need to do is define a type that will act as a suitable constructor for our mixin. We could name this type anything, but the convention that has evolved around mixins in TypeScript is to use the following type:

type Constructor<T ={}> = new(...args: any[]) => T;

This type definition gives us something that we can extend to create our specialized mixins. The strange-looking syntax effectively says that, given any particular type, a new instance will be created using any appropriate arguments.

Here is our record status implementation:

function RecordStatus<T extends Constructor>(base : T) {
return class extends base {
Deleted : boolean = false;
}
}

The RecordStatus function extends the Constructor type by returning a new class that extends the constructor implementation. In this, we add our Deleted flag.

To merge or mix in these two types, we simply do the following:

const ActivePerson = RecordStatus(Person);

This has created something we can use to create a Person object with RecordStatus properties. It has not actually instantiated any objects yet. To do that, we instantiate the information in the same way we would with any other type:

let activePerson = new ActivePerson("Peter", "O'Hanlon");
activePerson.Deleted = true;

Now, we also want to add details about when the record was last updated. We create another mixin, as follows:

function Timestamp<T extends Constructor>(base : T) {
return class extends base {
Updated : Date = new Date();
}
}

To add this to ActivePerson, we change the definition to include Timestamp. It does not matter which mixin we put first, whether it is Timestamp or RecordStatus:

const ActivePerson = RecordStatus(Timestamp(Person));

As well as properties, we can also add constructors and methods to our mixins. We are going to change our RecordStatus function to log out when the record was deleted. To do this, we are going to convert our Deleted property into a getter method and add a new method to actually perform the deletion:

function RecordStatus<T extends Constructor>(base : T) {
return class extends base {
private deleted : boolean = false;
get Deleted() : boolean {
return this.deleted;
}
Delete() : void {
this.deleted = true;
console.log(`The record has been marked as deleted.`);
}
}
}

A word of warning about using mixins like this. They are a great technique, and they provide the ability to neatly do some really useful things, but we cannot pass them as a parameter unless we relax the parameter restrictions to any. That means we cannot use code like this:

function DeletePerson(person : ActivePerson) {
person.Delete();
}
If we look at mixins in the TypeScript documentation at  https://www.typescriptlang.org/docs/handbook/mixins.html, we see that the syntax looks very different. Rather than dealing with that approach, with all of the inherent limitations it has, we will stick with the method here, which I was first introduced to at  https://basarat.gitbooks.io/typescript/docs/types/mixins.html.