π Mixins in Typescript π
Aravind V
Posted on February 13, 2022
Mixins is a popular way of building up classes from reusable components by combining simpler partial classes.
In this article we are trying to demonstrate how we can use them in typescript.
Identify the Base Class π«
We will start this by creating a base class like the below one:
class Book {
name = "";
constructor(name: string) {
this.name = name;
}
}
Define a type definition focusing on our base class β‘
Define a type definition which is used to declare that the type being passed, is nothing but a typical class.
type Constructor = new (...args: any[]) => {};
Class expression way to define a mixin πΏ
Define the factory function which will return a class expression, this function is what we call Mixin
here.
function Pages1<TBase extends Ctr>(Base: TBase) {
return class Pages extends Base {
_pages = 1;
setPages(pages: number) {
this._pages = pages;
}
get Pages(): number {
return this._pages;
}
};
}
Time to use the mixin to derive classes βοΈ
Let us use this newly created mixin to create a new classes as follows:
const PagedBook = Pages1(Book);
const HP = new PagedBook("Harry Potter and the Sorcerer's Stone");
HP.setPages(223);
console.log(`${HP.name} - ${HP.Pages}`);
const AW = new PagedBook("Alice's Adventures in Wonderland");
AW.setPages(353);
console.log(`${AW.name} - ${AW.Pages}`);
In the above example, this many sound weird at first sight that the same could be easily defined in the earlier base class itself, but what we have achieved is that we are able to generate new subclass by combining partial class at runtime, based on our requirement. And hence it is powerful.
Constrained Mixins π
We can also make our Ctr
type defined earlier more generic by using the below changes.
type GenCtr<T = {}> = new (...args: any[]) => T;
type BookCtr = GenCtr<Book>;
But why do we need to use this new generic constructor, this is to make sure we can constrain by choosing the right base class features before we can extend with our mixin.
function Pages2<TBase extends BookCtr>(Base: TBase) {
return class Pages extends Base {
_pages = 1;
setPages(pages: number) {
this._pages = pages;
}
get Pages(): number {
return this._pages;
}
};
}
The above works the same way as the previous mixin, but we have just demonstrated the use of constraints by using mixins to build classes.
const PagedBook = Pages2(Book);
const HP = new PagedBook("Harry Potter and the Sorcerer's Stone");
HP.setPages(223);
console.log(`${HP.name} - ${HP.Pages}`);
const AW = new PagedBook("Alice's Adventures in Wonderland");
AW.setPages(353);
console.log(`${AW.name} - ${AW.Pages}`);
Another example π
Another example to add more notes by what we just meant.
type AuthorCtr = GenCtr<{ setAuthor: (author: string) => void }>;
function AuthoredBook<TBase extends AuthorCtr>(Base: TBase) {
return class AuthoredBook extends Base {
Author(name: string) {
this.setAuthor(name)
}
};
}
In the segment above we have created a type which expects the base class to have a method setAuthor
which takes a param author so that the mixin could be applied to extend the base classes. This is one of the ways to create a constrained mixin.
Why do we need to add constraints β
- we can identity the constraint, it will help us write the mixin easily targeting the required features with the right set of dependancy at the same time.
- second one this is that typescript we do this everywhere making well defined types, so that we are may easily understand the block at the same time tsc will always remind us when we commit error, besides giving us inference while coding.
This also let us define abstract mixins which are loosely coupled targeting only the specific features and can be chainned as per the necessity as in the below example.
class Novel {
_name = "";
_author= "";
constructor(name: string) {
this._name = name;
}
setAuthor(author: string){
this._author=author;
}
about():string {
return `${this._name} by ${this._author}`
}
}
The above code snippet used a raw Novel
class, here we can do some mixins to achieve the desirable effects.
First let us define them as follows;
type Authorable = GenCtr<{ setAuthor: (author: string) => void }>;
function AuthoredBook<TBase extends Authorable>(Base: TBase) {
return class AuthoredBook extends Base {
author(fname: string, lname: string) {
this.setAuthor(`${fname} ${lname}`)
}
};
}
type Printable = GenCtr<{ about: () => string }>;
function PrintBook<TBase extends Printable>(Base: TBase) {
return class PrintBook extends Base {
print() {
return `***** `+this.about()+` ******`
}
};
}
In the above code snippet we defined couple of mixins, which is loosely coupled with any base class as it only expect specific methods to mix &enhance it.
const StoryBook1 = AuthoredBook(Novel);
const PrintableBook1 = PrintBook(StoryBook1);
const Sb1 = new PrintableBook1("Gulliverβs Travel");
Sb1.author("Jonathan", "Swift");
console.log(Sb1.print());
π What is cool about using mixins it that the order in which the chaining occurs is not important, still the results will be consistent since the partial class features mix one after other as they are applied.
const PrintableBook2 = PrintBook(Novel);
const StoryBook2 = AuthoredBook(PrintableBook2);
const Sb2 = new StoryBook2("Gulliverβs Travel");
Sb2.author("Jonathan", "Swift");
console.log(Sb1.print());
π Original post at π Dev Post
Thanks for supporting! π
Would be really great if you like to β Buy Me a Coffee, to help boost my efforts.
Posted on February 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.