๐ŸŒ€ Mixins in Typescript ๐Ÿ€

๐ŸŒ€ Mixins in Typescript ๐Ÿ€

ยท

4 min read

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());

๐ŸŽ‰ Thanks for supporting! ๐Ÿ™

Would be great if you like to โ˜• Buy Me a Coffee, to help boost my efforts.

๐Ÿ” reposted at ๐Ÿ”— dev to @aravindvcyber

Did you find this article valuable?

Support Aravind V by becoming a sponsor. Any amount is appreciated!

ย