Tuesday, February 27, 2018

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

Leave a Comment

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?

2 Answers

Answers 1

I'm not sure what your are trying to achive here, but I think that you are missing the original A and B classes inside Fooable.

A quick fix with a couple of casts:

const Fooable: DomainExtender<Foo, {}> =      <A extends Alpha, B extends Beta>({ Alpha, Beta }: Domain<A, B>) => {      class AlphaWithFoo extends (Alpha as Constructor<{}>) {         foo: string = 'foo';     };      return { Alpha: AlphaWithFoo as Constructor<A & Foo>, Beta }; } 

Here the DomainExtender is not very useful, because the full signature of the function must be repeated.

Answers 2

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 }; } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment