Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multi-mixin helper leads to unexpected non-assignable compiler error. Am I doing it wrong?

Tags:

typescript

I'm trying to type a multi-mixin helper function that accepts a map of constructors and returns a map of those constructors extended with some new interface.

Consider the following contrived base classes:

class Alpha { alpha: string = 'alpha'; };
class Beta { beta: string = 'beta'; };

I have a mixin helper function that accepts a map of these classes and returns optionally extended versions of them. Without typings, it looks like this:

// The multi-mixin helper function:
const Fooable = ({ Alpha, Beta }) => {
  const AlphaWithFoo = class extends Alpha {
    foo = 'foo';
  };

  return { Alpha: AlphaWithFoo, Beta };
}

// When invoked, it might be used like this:
const { Alpha: FooAlpha, Beta: FooBeta } = Fooable({ Alpha, Beta });
const fooAlpha = new FooAlpha();

// fooAlpha has both "alpha" and "foo" propoerties:
console.log(fooAlpha.alpha, fooAlpha.foo);

However, when I fully type this helper function out, my typings lead to a very strange not-assignable compiler error. Here is a link to the TypeScript compiler playground where you can see the typings I'm using, and the associated error:

live example in typescriptlang.org playground

The error I'm getting reads:

Type '<A extends Alpha, B extends Beta>({ Alpha, Beta }: Domain<A, B>) => { Alpha: typeof AlphaWithFoo;...' is not assignable to type 'DomainExtender<Foo, {}>'.
  Type '{ Alpha: typeof AlphaWithFoo; Beta: Constructor<B>; }' is not assignable to type 'ExtendedDomain<A, B, Foo, {}>'.
    Types of property 'Alpha' are incompatible.
      Type 'typeof AlphaWithFoo' is not assignable to type 'Constructor<A & Foo>'.
        Type 'AlphaWithFoo' is not assignable to type 'A & Foo'.

This error seems kind of strange to me. It is as though the compiler is losing track of the type information for the Alpha class or something. But, it is probably more likely that I am making a mistake somewhere.

Can anyone help me figure out where I went wrong?

like image 838
cdata Avatar asked Feb 17 '18 23:02

cdata


1 Answers

There is a quirk in the way you can derive from a generic parameter, the generic parameter must extend new(...args: any[]) => any (parameters MUST be ...args: any[]). We can see this in action in this PR. To get around this we can use a helper function inside the Fooable function:

const Fooable: DomainExtender<Foo, {}> = ({ Alpha, Beta }) => {
    function mixin<U extends Constructor>(Base: U) {
        return class extends Base {
            constructor(...args: any[]) {
                super(...args);
            }
            foo: string = 'foo';
        }
    }
    const AlphaWithFoo = mixin(Alpha);

    return { Alpha: AlphaWithFoo, Beta };
}
like image 174
Titian Cernicova-Dragomir Avatar answered Nov 06 '22 19:11

Titian Cernicova-Dragomir