Applying a typescript mix-in to a generic base class to obtain a generic derived class

Viewed 440

In typescript, I have defined an interface with a type parameter T. It's for some kind of container over values of type T, which can be implemented in multiple way. Some of the methods on the interface need to be specialized for each implementation. But other methods can be given a generic implementation (over T) where the result can be obtained solely by calling the core methods. So I thought I could implement this by using a mix-in to extend a class that just implements the core methods to one that implements all of the methods.

But I've not been able to discover valid typescript syntax that will apply a mix-in to a generic base class to produce a generic derived class.

Let me give a simplified example. I split my interface into a core part (Base<T>) and a derived part (Derived<T>).

interface Base<T> {
    foo(x: number): T[];
}

interface Derived<T> extends Base<T> {
    doubleFoo(x: number): T[];
}

I can implement a class on the base interface, say like this:

class BaseImpl<T> implements Base<T> {
    constructor(public t: T) {}

    foo(x: number) {
        return new Array(x).fill(this.t);
    }
}

Now I want to write a mix-in function that given BaseImpl will return a class that implements Derived. Both base and derived implementation classes should still have T as a type parameter.

I initially tried the following:

type Constructor<I> = new (...args: any[]) => I;

const MixInDoubleFoo = <T, C extends Constructor<Base<T>>> (BaseClass: C) => {
    return class extends BaseClass implements Derived<T> {

        doubleFoo(x: number) {
            return this.foo(x * 2);
        }
    }
}

const DerivedImpl = MixInDoubleFoo(BaseImpl);

This compiles without error. But it seems that the resulting class has lost some type information. When I tried the following I got an error.

class MyDerivedFromMixin<T> extends DerivedImpl<T> {

    tripleFoo(x: number): T[] {
        // error: Type 'unknown[]' is not assignable to type 'T[]'.
        // error:   Type 'unknown' is not assignable to type 'T'.
        // error:     'T' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.
        return this.foo(3 * x);
    }
}

For contrast if I try to extend the base implementation class in exactly the same way it compiles without error.

class MyDerivedFromBaseImpl<T> extends BaseImpl<T> {

    tripleFoo(x: number): T[] {
        return this.foo(3 * x);
    }
}

It appears to me that the typescript type inference failed to view T as a type parameter of the function returned from the mix-in, but replaced it with unknown.

I read over this question: Why does this TypeScript mixin employing generics fail to compile?. The situation in that question is different - the questioner is trying to add a type parameter into the mix-in function, whereas I am trying to lift up a type parameter from the base class.

Nevertheless it did tell me that typescript does not infer types when some, but not all, of the template parameters are provided. I experimented with both solutions suggested there (currying the mixin function, providing an extra dummy parameter to make type inference simpler). But although I tried lots of different syntaxes, none of them were acceptable to the compiler.

What I want to end up with is a mix-in which will allow me to write DerivedImpl = MixIn(BaseImpl) so that I can then:

  • Create an instance of the class: const instance = new DerivedImpl<T>(...)
  • Extend it as a class parameterized over T: class Extension<T> extends DerivedImpl<T> { ... }

Is there any way to do this? I'm starting to think that I should just give-up on trying to use a mix-in, and just make the 'derived' methods into template functions over T that take an instance of the base class. That will obviously work, it just seems somewhat ugly.


Typescript playground containing all code from above: playground

I'm using typescript 3.9.

0 Answers
Related