4

Mixins in TypeScript

 2 years ago
source link: https://blog.bitsrc.io/mixins-in-typescript-32c6e28cf40f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Mixins in TypeScript

Mixins are a popular way of building up classes from reusable components by combining simpler partial classes.

In this article we will demonstrate how we can use mixins 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[]) => {};

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;
}
};
}

Use the mixin to derive a class

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, it may seem weird, as we could have defined this in the earlier base class itself. But we were able to generate a new subclass by combining a partial class at runtime, based on our requirement.

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?

The first reason is so that we can identity the constraint. Knowing this will help us write the mixin easily, targeting the required features with the right set of dependancy at the same time.

The second reason is so that we are can easily understand the block at the same time, as TypeScript will always remind us when we commit error.

This will also enable us to define abstract mixins that are loosely coupled, targeting only the specific features and can be chained 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 are loosely coupled with any base class, as it only expect specific methods to mix and 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 is that the order in which the chaining occurs is not important; the results will remain 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());

Build composable web applications

Don’t build web monoliths. Use Bit to create and compose decoupled software components — in your favorite frameworks like React or Node. Build scalable frontends and backends with a powerful and enjoyable dev experience.

Bring your team to Bit Cloud to host and collaborate on components together, and greatly speed up, scale, and standardize development as a team. Start with composable frontends like a Design System or Micro Frontends, or explore the composable backend. Give it a try →

Learn More


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK