CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ts-mixer

A TypeScript library that provides tolerable Mixin functionality with multiple inheritance support.

Pending
Overview
Eval results
Files

mixin-detection.mddocs/

Mixin Detection

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.

Capabilities

hasMixin Function

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 ✅

Type Narrowing

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 pass

Advanced Usage Examples

Deep Mixin Hierarchies

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

Abstract Class Support

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
}

Conditional Logic with Multiple Mixins

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"

Implementation Details

Search Algorithm

hasMixin uses a breadth-first search algorithm to traverse:

  1. Direct instanceof check: First checks regular instanceof
  2. Mixin constituents: Examines registered mixin components
  3. Prototype chain: Traverses the prototype chain for inheritance
  4. Recursive search: Continues until all paths are exhausted

Performance Considerations

  • Uses WeakMap for mixin tracking to avoid memory leaks
  • Breadth-first search prevents infinite loops in circular references
  • Caches visited classes during search to avoid redundant checks

Limitations

  • Only works with classes mixed using ts-mixer
  • Requires ES6 Map support (or polyfill)
  • Performance is O(n) where n is the size of the mixin hierarchy

Install with Tessl CLI

npx tessl i tessl/npm-ts-mixer

docs

configuration.md

core-mixing.md

decorators.md

generic-classes.md

index.md

mixin-detection.md

tile.json