A TypeScript library that provides tolerable Mixin functionality with multiple inheritance support.
—
Type-safe mixin detection and type narrowing functionality. Since instanceof doesn't work with mixins created by ts-mixer, the hasMixin function provides an equivalent capability with full type narrowing support.
Determines whether an instance has a specific mixin, providing type narrowing similar to instanceof.
/**
* Determines whether an instance has a specific mixin
* Provides type narrowing similar to instanceof
* @param instance - Object to test for mixin presence
* @param mixin - Class constructor to check for
* @returns Type guard indicating if instance has the mixin
*/
function hasMixin<M>(
instance: any,
mixin: abstract new (...args) => M
): instance is M;Basic Usage:
import { Mixin, hasMixin } from "ts-mixer";
class Foo {
fooProperty: string = "foo";
fooMethod(): string {
return "from foo";
}
}
class Bar {
barProperty: string = "bar";
barMethod(): string {
return "from bar";
}
}
class FooBar extends Mixin(Foo, Bar) {
combinedMethod(): string {
return this.fooMethod() + " and " + this.barMethod();
}
}
const instance = new FooBar();
// instanceof doesn't work with mixins
console.log(instance instanceof FooBar); // true
console.log(instance instanceof Foo); // false ❌
console.log(instance instanceof Bar); // false ❌
// hasMixin works correctly
console.log(hasMixin(instance, FooBar)); // true
console.log(hasMixin(instance, Foo)); // true ✅
console.log(hasMixin(instance, Bar)); // true ✅The hasMixin function provides full TypeScript type narrowing:
import { Mixin, hasMixin } from "ts-mixer";
class Drawable {
draw(): void {
console.log("Drawing...");
}
}
class Movable {
x: number = 0;
y: number = 0;
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
}
class Shape extends Mixin(Drawable, Movable) {
area: number = 0;
}
function processObject(obj: any) {
// Type narrowing with hasMixin
if (hasMixin(obj, Drawable)) {
obj.draw(); // ✅ TypeScript knows obj has draw() method
}
if (hasMixin(obj, Movable)) {
obj.move(10, 20); // ✅ TypeScript knows obj has move() method
console.log(obj.x, obj.y); // ✅ TypeScript knows obj has x, y properties
}
if (hasMixin(obj, Shape)) {
obj.draw(); // ✅ Available from Drawable
obj.move(5, 5); // ✅ Available from Movable
console.log(obj.area); // ✅ Available from Shape
}
}
const shape = new Shape();
processObject(shape); // All type checks passhasMixin works with complex mixin hierarchies and nested mixins:
import { Mixin, hasMixin } from "ts-mixer";
class A {
methodA(): string { return "A"; }
}
class B {
methodB(): string { return "B"; }
}
class AB extends Mixin(A, B) {
methodAB(): string { return "AB"; }
}
class C {
methodC(): string { return "C"; }
}
class D {
methodD(): string { return "D"; }
}
class CD extends Mixin(C, D) {
methodCD(): string { return "CD"; }
}
// Mixing already-mixed classes
class SuperMixed extends Mixin(AB, CD) {
superMethod(): string {
return "Super";
}
}
class ExtraLayer extends SuperMixed {}
const instance = new ExtraLayer();
// hasMixin traverses the entire hierarchy
console.log(hasMixin(instance, A)); // true
console.log(hasMixin(instance, B)); // true
console.log(hasMixin(instance, AB)); // true
console.log(hasMixin(instance, C)); // true
console.log(hasMixin(instance, D)); // true
console.log(hasMixin(instance, CD)); // true
console.log(hasMixin(instance, SuperMixed)); // true
console.log(hasMixin(instance, ExtraLayer)); // true
// Type narrowing works at any level
if (hasMixin(instance, A)) {
instance.methodA(); // ✅ Available
}
if (hasMixin(instance, CD)) {
instance.methodC(); // ✅ Available from C
instance.methodD(); // ✅ Available from D
instance.methodCD(); // ✅ Available from CD
}hasMixin works with abstract classes:
import { Mixin, hasMixin } from "ts-mixer";
abstract class Animal {
abstract makeSound(): string;
name: string;
constructor(name: string) {
this.name = name;
}
}
abstract class Flyable {
abstract fly(): string;
altitude: number = 0;
ascend(height: number): void {
this.altitude += height;
}
}
class Bird extends Mixin(Animal, Flyable) {
constructor(name: string) {
super(name);
}
makeSound(): string {
return "Tweet!";
}
fly(): string {
return `${this.name} is flying at ${this.altitude} feet`;
}
}
const bird = new Bird("Robin");
console.log(hasMixin(bird, Animal)); // true
console.log(hasMixin(bird, Flyable)); // true
console.log(hasMixin(bird, Bird)); // true
// Type narrowing with abstract classes
if (hasMixin(bird, Animal)) {
console.log(bird.name); // ✅ Available
console.log(bird.makeSound()); // ✅ Available
}
if (hasMixin(bird, Flyable)) {
bird.ascend(100); // ✅ Available
console.log(bird.fly()); // ✅ Available
}import { Mixin, hasMixin } from "ts-mixer";
interface Serializable {
serialize(): string;
}
class JsonSerializable implements Serializable {
serialize(): string {
return JSON.stringify(this);
}
}
interface Cacheable {
cacheKey: string;
}
class SimpleCacheable implements Cacheable {
cacheKey: string;
constructor(key: string) {
this.cacheKey = key;
}
}
interface Loggable {
log(message: string): void;
}
class ConsoleLoggable implements Loggable {
log(message: string): void {
console.log(`[${new Date().toISOString()}]: ${message}`);
}
}
// Various combinations of mixins
class DataObject extends Mixin(JsonSerializable, SimpleCacheable) {
data: any;
constructor(data: any, cacheKey: string) {
super(cacheKey);
this.data = data;
}
}
class LoggableDataObject extends Mixin(DataObject, ConsoleLoggable) {}
function processObject(obj: any) {
let capabilities: string[] = [];
if (hasMixin(obj, JsonSerializable)) {
capabilities.push("serializable");
const serialized = obj.serialize(); // ✅ Type-safe access
}
if (hasMixin(obj, SimpleCacheable)) {
capabilities.push("cacheable");
const key = obj.cacheKey; // ✅ Type-safe access
}
if (hasMixin(obj, ConsoleLoggable)) {
capabilities.push("loggable");
obj.log("Processing object"); // ✅ Type-safe access
}
console.log(`Object has capabilities: ${capabilities.join(", ")}`);
}
const dataObj = new DataObject({ value: 42 }, "my-key");
const loggableDataObj = new LoggableDataObject({ value: 100 }, "logged-key");
processObject(dataObj); // "serializable, cacheable"
processObject(loggableDataObj); // "serializable, cacheable, loggable"hasMixin uses a breadth-first search algorithm to traverse:
WeakMap for mixin tracking to avoid memory leaksMap support (or polyfill)Install with Tessl CLI
npx tessl i tessl/npm-ts-mixer