Typescript: Mixin does not allow Generic

Created on 2 Aug 2018  Β·  10Comments  Β·  Source: microsoft/TypeScript


TypeScript Version: 3.1.0-dev.20180802


Search Terms:

  • Typescript Mixins Generics
  • Base class expressions cannot reference class type parameters.

Code

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

interface BasicTypes {
  user: {name: string}
}

class BasicHandler<E extends BasicTypes> {
    // some methods here
}

interface TypesA extends BasicTypes {
   user: BasicTypes["user"] & {img: string}
}

interface TypesB extends BasicTypes {
    user: BasicTypes["user"] & {email?: string}
}

function UserMixin<E extends BasicTypes, B extends Constructor<BasicHandler<E>>>(Base: B)  {
    class UserHandler extends Base { //
        constructor(...args: any[]) {
            super(...args);
        }
        setUser(user: E["user"]) {
            //...
        }
    }
    return UserHandler;
}

// error TS2562: Base class expressions cannot reference class type parameters.
class HandlerA<MergedTypes extends TypesA> extends UserMixin<MergedTypes, Constructor<BasicHandler<MergedTypes>>>(BasicHandler) {
   // some other specific methods here
}

// error TS2562: Base class expressions cannot reference class type parameters.
class HandlerB<MergedTypes extends TypesB> extends UserMixin<MergedTypes, Constructor<BasicHandler<MergedTypes>>>(BasicHandler) {
    // some other specific methods here
 }

type mergedTypes = TypesA & TypesB

const currentUser: mergedTypes["user"] = {
    name: "Peter Parker",
    img: "https://....",
    email: "[email protected]"
}

const a: HandlerA<mergedTypes> = {} as any;
a.setUser(currentUser)

const b: HandlerB<mergedTypes> = {} as any;
b.setUser(currentUser);

Expected behavior:
Mixins should allow generic types and propagate them to the inherited class

Actual behavior:
Cannot set generic type to mixin

Playground Link: Link

Related Issues:

Working as Intended

Most helpful comment

@RyanCavanaugh, what is the reason why it is considered that the inheritance list should be outside of the scope of the class declaration? (which is the root of this problem)

It's very counter intuitive and, right now, I can't think of any reason why it should be like that.

All 10 comments

@RyanCavanaugh, what is the reason why it is considered that the inheritance list should be outside of the scope of the class declaration? (which is the root of this problem)

It's very counter intuitive and, right now, I can't think of any reason why it should be like that.

@rzvc the type parameters of a class are not in scope in the static side of its declaration -- this should be noncontroversial as the static side of a class is a singleton and cannot vary over any type parameters (there is only one constructor function, not one class per instantiated type).

It follows that instance-specific type parameters are not in scope in the base class expression, because the base class expression is only evaluated once -- when Foo<T> extends SomeExpr, SomeExpr is only evaluated once and cannot vary over T because there isn't a T yet.

@RyanCavanaugh, I'm sorry but I still don't get it. I'm not sure what you mean by the static side, as in my mind, it's all static (doesn't matter what types you feed in, you get the same code - I'm probably misunderstanding what you mean). You seem to imply that something would have to vary and I'm not sure what.

Can you please provide an explanation on the following code, on why exactly should T not be passed to test<T>(), in the inheritance list of Foo, when the type checker does its thing?

function test<T>(): new () => T
{
    return null;
}

class Foo<T> extends test<T>()
{
    public constructor()
    {
        super();
    }
}

@rzvc let's say you wrote

new Foo<string>();
new Foo<number>();

How many times is test invoked?

The answer is 1, because test only gets invoked once, during the initialization of the class declaration.

This is different from Foo<T> - its constructor runs twice and can plausibly be seeing actual values of type T.

So it makes no sense for string and number to be provided at test, because there's no possible way that test is actually generic over T - its behavior cannot vary over its type parameter, because it can never deal with an actual value of T during its lifetime.

Put another way, let's say you wrote this:

function test<T>(argument: T): new () => T
{
    return null;
}

class Foo<T> extends test<T>(/* provide an argument */)
{
    public constructor()
    {
        super();
    }
}

There's nothing to "fill in the blank" with - test is lying about being generic; it doesn't actually vary over its argument because it has no arguments.

@RyanCavanaugh, ok, I see what you mean, but it's not the whole story. What we really want to have as a generic is the return type, which is a constructor signature and gets called every time the class is instantiated.

Perhaps the type checker could reject the function call if it has generic parameters, that rely on the type of the base class, but not if just the return type does. In the end what we care about is for the returned constructor signature to be dependent on T.

Edit: Rephrased something.

What seems weird to me is that this is okay:

class Foo<T> { }

class Bar<T> extends Foo<T> { }

Passing generics to anonymous classes is also okay:

function generateFoo() {
    return class<T> { }
}

const Foo = generateFoo();

class Bar<T> extends Foo<T> { }

However if we try to make the anonymous class inherit from a base class received as a parameter (mixin style), it breaks:

class Baz { }

export type Constructor<T = {}> = new (...args: any[]) => T;

function generateFoo<T extends Constructor<{}>>(Base: T) {
    return class<B> extends Base {
        constructor(...args: any[]) {
            super(...args);
        }
    }
}

const Foo = generateFoo(Baz);

class Bar<T> extends Foo<T> { }

Right of the bat it complains that "A mixin class must have a constructor with a single rest parameter of type 'any[]'" (which it does). It's the generic B that breaks it.

In my mind the result of generateFoo(Baz) should still be:

class Foo<B> extends Baz { }

Y u do dis typscrip

@RyanCavanaugh

let's say you wrote

new Foo<string>();
new Foo<number>();

How many times is test invoked?

The answer is 1

Correct me if I am wrong, but if the similar generics code is in c++, there will definitely be two Foo classes, and test() would be invoked twice (I know c++ doesn't have this mixin mechanism, just assume similar mechanism exist in c++).
Would this be where the problem come from?

We are running into the same issues trying to create very low level React mixins for specialized components.

In other languages, including C++ generics become separate "copies". I believe C++ called them templates at one point.

Not only could this solve a number of issues that we are having, but it would also allow static properties to access generics, along with keeping a separate static property value per generic type, and perhaps other benefits as well.

I know I'm a little late to the party, but ts-mixer might be able to help. It has a decent solution to the generics problem.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MartynasZilinskas picture MartynasZilinskas  Β·  3Comments

uber5001 picture uber5001  Β·  3Comments

fwanicka picture fwanicka  Β·  3Comments

blendsdk picture blendsdk  Β·  3Comments

manekinekko picture manekinekko  Β·  3Comments