Search code examples
typescriptmixinstyping

Typescript mixin function with optional superClass


I'm trying to build a mixin function that optionally expects a super class. The rationale is that we often just build intermediate classes to start off with our mixins. I am quite confident with the following declarations, but they don't work, however:

interface Test {
  readonly __TEST: "test";
  new (...args: any[]): {
    readonly test: "test";
  };
}

function TestMixin<SuperClass extends new (...args: any[]) => any>(
  superClass?: SuperClass
) {
  const defaultClass = class {};
  /* Error: Type 'new (...args: any[]) => any' is not assignable to type 'SuperClass extends undefined ? typeof defaultClass : undefined'.ts(2322) */
  const sc: typeof superClass extends undefined ? typeof defaultClass : undefined = superClass === undefined ? defaultClass : superClass;

  /* Error: Type 'SuperClass extends undefined ? typeof defaultClass : undefined' is not a constructor function type. */
  class T extends sc implements InstanceType<Test> {
    public static readonly __TEST = "test";
    public readonly test = "test";
  }

  return T;
}

Solution

  • You can't extend a conditional type, Typescripts expects the clause in the extends clause to be a constructor, not any other more complicated type.

    I think the simplest solution in this case would be to lie to the compiler with a type assertion:

    interface Test {
        readonly __TEST: "test";
        new(...args: any[]): {
            readonly test: "test";
        };
    }
    
    function TestMixin<SuperClass extends new (...args: any[]) => any = new () => {}>(
        superClass?: SuperClass
    ) {
        const defaultClass = class { };
        /* ok */
        const sc = (superClass === undefined ? defaultClass : superClass) as SuperClass;
    
        /* Ok now */
        class T extends sc implements InstanceType<Test> {
            public static readonly __TEST = "test";
            public readonly test = "test";
        }
    
        return T;
    }
    
    let a = TestMixin();
    new a().test;
    
    let b = TestMixin(class {
        constructor(n: number) { console.log("Hi") }
        m() { }
    });
    new b(1).test;
    new b(1).m();
    

    As long as nobody specifies an explicit type parameter to TestMixin and omits the parameter, it should work fine.