typescriptmixins

How to check if a class has a mixin applied in TypeScript? (inferring its type)


I am trying to determine if a TypeScript class has a specific mixin applied to it in a way that narrows its type (for autocompletion/intellisense), but I am encountering some difficulties. I have tried two different approaches, but neither has been successful.

Option 1 (link to playground): Adding a name to the class inside the mixin. This approach did not work as the class is not defined inside the mixin but rather when the mixin is called. As a result, instanceof does not recognize it. Here is the code:

class A { }

let BMixin = (superclass: any) => class B extends superclass {
  bProp: string = "bProp";
  bMethod() {
    console.log("method from B");
  }
};

const C = BMixin(A)
const c = new C;
console.log(c instanceof B) // Cannot find name 'B'

Option 2 (link to playground): Creating a utility that receives a class and returns a mixin. The problem with this approach is that (a) I would have to find a way to copy all the methods and properties and (b) instanceof keeps returning false. Here is the code:

class A { }

class B {
  bProp: string = "bProp";
  bMethod() {
    console.log("method from B");
  }
}

function defineMixin (mixinClass: any) {
  return (arg0: any) => class extends arg0 {
    // Here I should do some magic to copy the methods and properties
  }
}

const D = defineMixin(B)(A)
const d = new D;
console.log(d instanceof B) // false

I found this post in which @elias-schablowski gets the type of the resulting class to have the methods and properties of the mixins. However, the Typescript compiler doesn't really get to know the prototype chain, and therefore you can't narrow the types of an arbitrary class.

In other words, I want to be able to do:

if (something instanceof someMixin) { // ¿Or ~ hasSomeMixin(something)?
    // here I want "something" to have the autocomplete
    // with all the properties and methods of "someMixin".
}

And also this:

const Something = CMixin(BMixin(A))
const something = new Something;
// here I want "something" to have the autocomplete
// with all the properties and methods of "A", "B" and "C".

I also found that the ts-mixer library offers an alternative function to instanceof called hasMixin that serves this purpose. The problem is that this library (~2kb GZipped) modifies the base class prototype in a very complex way, and only works under certain restrictions. E.g., I understand that this and super cannot be used 👎.

I would like a simpler solution that works with all the benefits of chaining prototypes without modifying them in some strange way.

How can I do this? I would appreciate any help.


Solution

  • Ok I was able to solve it.

    1. Context

    Suppose a BMixin that can only receive subclasses of class A.

    class A { }
    
    
    type Ctor<T> = new (...args: any[]) => T;
    type SubA = Ctor<A>
    
    function BMixin<TBase extends SubA>(Base: TBase) {
      return class B extends Base {
        bProp: string = "bProp";
        bMethod() {
          console.log("method from B");
        }
      };
    }
    
    

    2. SOLUTION: generic type guard

    We can define a guard type as follows:

    export type Block<T extends SubA = SubA> =
      InstanceType<ReturnType<typeof BMixin<T>>>;
    
    function hasBMixin<T extends SubA>(
      input: any,
      klass?: ReturnType<typeof BMixin<T>>
    ): input is Block<T> {
      // any runtime check that lets you know that input has the mixin
      if (!klass) return "bProp" in input;
      return input instanceof klass;
    }
    

    3. USAGE: simple case

    And it is used as follows:

    const a = "a" as unknown;
    if (hasBMixin(a)) {
      a.bMethod()
    }
    

    4. USAGE: advanced case

    You can even narrow the type to a specific subclass of A

    class subA1 extends A {
      subA1Method() {
        console.log("method from subA1")
      };
    }
    const C = BMixin(subA1)
    
    if (hasBMixin(a, C)) {
      a.bMethod()
      a.subA1Method();
    }
    

    Link to the full example in the playground

    Credits to this subcomment by @GeoffHarper which inspired this solution.