Learn Typescript Mixins: Avoid Spaghetti Code - SaasEasy Blog

As a software developer, I’ve been using Typescript for most of my latest projects. One of the most interesting features I’ve come across is Typescript Mixins. These allow for simple composability of classes and functions, which can help to reduce code complexity and improve maintainability.

In this article, I’m going to provide a comprehensive guide on Typescript Mixins. I’ll start with the basics and then dive into more advanced features, giving you a solid understanding of how to use them in your projects.

What are Typescript Mixins?

Typescript Mixins are one of the Object-Oriented Programming (OOP) concepts, which allow for the composition of objects. According to the official Typescript documentation, a mixin is a way of adding functionality to an object by combining it with other objects. In simpler terms, a mixin allows you to add new properties and methods to a class, or even to a plain object, without having to modify the original implementation.

For example, let’s say I have a class that represents a car. It has a few methods like start and stop to control its engine. With mixins, I can add new methods, properties, or even a new class to the car class without modifying the original source code. This means that I can extend the functionality of the car class in a modular and reusable way.

Why Use Typescript Mixins?

Mixins offer some benefits in comparison to other OOP concepts, such as inheritance and composition. Inheritance can lead to complex class hierarchies, which can be hard to maintain. On the other hand, composition requires a lot of boilerplate code to reuse functionality across multiple classes. Mixins offer a good balance between these two concepts, as they allow for easy, modular, and reusable extension of class functionality without complicated hierarchies.

Moreover, Mixins avoid the pitfalls where adding functionality to objects can lead to over-engineering or spaghetti codes. This way, you can easily update, test and maintain functionalities across multiple objects.

Understanding the Concept of Mixins in TypeScript

To get started with Mixins, it’s important to understand their basic structure and syntax. A mixin is essentially a function that enhances an existing object with additional properties and methods. The mixin function takes an argument that represents the target object to be extended and returns an object that becomes its updated version.

In TypeScript, mixins are created using a combination of generic classes, intersection types, and utility types.

Here is a basic example of a Mixin:

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

function CarMixin<T extends Constructor>(Base: T) {
  return class extends Base {
    drive() {
      console.log('I am driving');
    }
  };
}

class Car {
  start() {
    console.log('Starting engine');
  }
  stop() {
    console.log('Stopping engine');
  }
}

const CarWithDrive = CarMixin(Car);

const myCar = new CarWithDrive();
myCar.start(); // Starting engine
myCar.stop(); // Stopping engine
myCar.drive(); // I am driving

Here, we have defined a CarMixin function, which takes a class as its argument. It returns a new class that is a combination of Car and drive method. We apply CarMixin to the Car class and create CarWithDrive, which has all the properties and methods of Car and the drive method added by the mixin.

The Three Main Types of TypeScript Mixins

There are three main types of mixins that can be created in TypeScript: Function-Based Mixins, Class-Based Mixins, and Object-Based Mixins.

Function-Based Mixins:

These involve creating a function that returns an object with the desired properties and methods. This type of Mixin is easy to implement and quite flexible.

function CarStaticMixin<T extends Constructor>(Base: T) {
  return class extends Base {
    static count: number = 0;
    increase() {
      (this.constructor as any).count++;
    }
  };
}

class Car {
  static count: number = 0;

  constructor(public color: String) {}

  start() {
    console.log('Starting engine');
  }
  stop() {
    console.log('Stopping engine');
  }
}

const CarWithMixin = CarStaticMixin(Car);
const car1 = new CarWithMixin('red');
const car2 = new CarWithMixin('blue');

console.log(car1.color); // red
console.log(car2.color); // blue

car1.increase();
console.log(CarWithMixin.count); // 1

car2.increase();
console.log(CarWithMixin.count); // 2

Here, we define CarStaticMixin as a function that adds a static property count and an instance method increase to the class. We then apply this Mixin to the Car class and create a new class CarWithMixin that has the new methods and properties.

Class-Based Mixins:

Class-Based Mixins involve creating a new class that extends the base class and adds new properties and methods to the mixed class. This type of Mixin is similar to inheritance, with the difference being that the Mixin class is not a subclass of the base class.

class Pet {
  walk() {
    console.log('Walking...');
  }
}

class Bird {
  fly() {
    console.log('Flying...');
  }
}

type PetConstructor = new (...args: any[]) => Pet;
type BirdConstructor = new (...args: any[]) => Bird;

function MammalMixin<TBase extends PetConstructor>(base: TBase) {
  return class extends base {
    run() {
      console.log('Running...');
    }
  };
}

function FlyMixin<TBase extends PetConstructor & BirdConstructor>(Base: TBase) {
  return class extends Base {
    fly() {
      console.log('Flying high...');
    }
  };
}

const RunningPet = MammalMixin(Pet);
const FlyingPet = FlyMixin(Pet);

class Bat extends MammalMixin(FlyMixin(Pet)) {}

const myPet = new RunningPet();
myPet.walk(); // Walking...
myPet.run(); // Running...

const myFlyingPet = new FlyingPet();
myFlyingPet.fly(); // Flying high...

const myBat = new Bat();
myBat.walk(); // Walking...
myBat.fly(); // Flying high...
myBat.run(); // Running...

In this example, we define two classes Pet and Bird. We also define two Mixin functions MammalMixin and FlyMixin, each of which returns a class that is a new combination of the base class and the new functionality.

We apply these Mixin functions to the Pet class and then create a new class called Bat, which inherits from both Pet and Bird, facilitated by various mixins. The Bat class now has all the combined properties and methods from the Pet, Mammal, and Bird classes.

Object-Based Mixins:

This type of Mixin involves adding new properties and methods to a plain object, rather than a class. This type of Mixin is commonly used to refactor code or centralize functionality.

type Movement = { move: () => void };
type Sound = { makeSound: () => void };
type Appearance = { color: string; height: number };

function CatAppearanceMixin(obj: Cat): Appearance {
  return { color: obj.color, height: obj.height };
}
function CatSoundMixin(cat: Cat): Sound {
  return { makeSound: () => console.log(`The ${cat.breed} cat is meowing!`) };
}
function CatMovementMixin(obj: Cat): Movement {
  return {
    move: () => console.log(`The ${obj.breed} cat is walking!`),
  };
}

interface Cat extends Appearance, Sound, Movement {
  breed: string;
}

class Siamese implements Cat {
  breed = 'Siamese';

  constructor(public color: string, public height: number) {}

  makeSound() {}
  move() {}
}

const myCat = new Siamese('brown', 30);

const catAppearance = CatAppearanceMixin(myCat);
console.log(catAppearance.color); // brown
console.log(catAppearance.height); // 30

const catSound = CatSoundMixin(myCat);
catSound.makeSound(); // The Siamese cat is meowing!

const catMovement = CatMovementMixin(myCat);
catMovement.move(); // The Siamese cat is walking!

In this example, we define three Mixin functions, each of which adds new functionality to the Cat instance. We pass in an instance of the Cat class to each Mixin function, and each returns an object containing the new functionalities.

We then use these Mixin functions to create a new Cat interface that has all the combined properties and methods from the base class and the Mixin functions.

Implementing Mixins in TypeScript

Now that you have a better understanding of Mixins, let’s look at how they can be implemented in TypeScript.

Step-by-Step Guide to Implementing Mixins:

  1. Define the base class
  2. Create the Mixin function(s)
  3. Apply the Mixin function(s) to the base class
  4. Use the new class as you would normally use the base class

Here is an example that demonstrates this sequence:

class Vehicle {
  protected type: string;

  constructor(type: string) {
    this.type = type;
  }

  printType() {
    console.log(`Type: ${this.type}`);
  }
}

Next, we’ll create a Mixin function that adds a new method to the Vehicle class:

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

function Printable<T extends Constructor>(base: T) {
  return class extends base {
    printInfo() {
      console.log(`This is a ${this.type} vehicle!`);
    }
  };
}

This Mixin function adds a new method printInfo to the class, which simply prints out a message indicating the type of vehicle.

Now, we can apply the Mixin function to the Vehicle class:

class Car extends Printable(Vehicle) {
  constructor() {
    super('car');
  }
}

const myCar = new Car();
myCar.printType(); // Type: car
myCar.printInfo(); // This is a car vehicle!

In this example, we apply the Printable Mixin to the Vehicle class to create a new class called Car. When an instance of Car is created, it has all the properties and methods of the Vehicle class, as well as the new method printInfo added by the Mixin function.

We can see that the new printInfo method works as expected, and we can access all the original properties and methods of the Vehicle class.

Typescript Mixins Best Practices

While Typescript Mixins can be an incredibly useful tool for developers, they should be used with care. Here are some best practices to keep in mind when using Mixins:

  1. Use Mixins to add small and modular functionality to your classes. If you find that your Mixins are becoming too complex or overreaching, it may be a sign that you need to refactor your codebase.
  2. Avoid using Mixins that modify the same properties or methods of a class. This can lead to conflicts and unexpected behavior.
  3. Use Mixins sparingly. While they can reduce code complexity and improve maintainability, overuse can make your code harder to read and understand.
  4. Test your Mixins thoroughly before using them in production code. This will help to ensure they work as expected and don’t cause any unexpected issues.

Conclusion

Typescript Mixins are a powerful tool that can help to simplify and modularize your code. By using Mixins, you can easily extend the functionality of your classes without creating complex class hierarchies or boilerplate code. Mixins offer a good balance of inheritance and composition, making them a useful addition to your OOP toolbox.

In this article, we’ve covered the basics of Mixins, as well as the three main types of Mixins used in TypeScript. We also provided a step-by-step guide to implementing Mixins in your own code, and some best practices to keep in mind when using them.

Hopefully, this comprehensive guide has given you a solid understanding of Typescript Mixins, and how you can use them to improve your own code. Happy coding!

Mastering Typescript Enums
Typescript

Mastering Typescript Enums

As a web developer, I’m constantly learning new ways to improve my coding practices. One of the more recent additions to my toolkit is TypeScript, a powerful superset of JavaScript that provides additional features to make coding easier and more efficient. One of my favorite features of TypeScript is enums, which allow me to better […]

Typescript Utility Types
Typescript

Typescript Utility Types

Introduction Hi there, I’m excited to talk to you today about Typescript Utility Types. First, let’s provide a brief overview of what Typescript is for those who may not be familiar with it. Typescript is an open-source programming language that builds on top of JavaScript by adding static types to code. This can help catch […]

Typescript Declaration Merging
Typescript

Typescript Declaration Merging

Introduction Typescript Declaration Merging is an essential feature of the Typescript language, which allows developers to extend existing code without modifying it. It can be a bit complex and overwhelming for beginners, but once you understand how it works, you’ll find it to be a powerful tool. As a developer, you will come across scenarios […]