You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Base class expressions cannot reference class type parameters.
Code
typeConstructor<T>=new(...args: any[])=>T;interfaceBasicTypes{user: {name: string}}classBasicHandler<EextendsBasicTypes>{// some methods here}interfaceTypesAextendsBasicTypes{user: BasicTypes["user"]&{img: string}}interfaceTypesBextendsBasicTypes{user: BasicTypes["user"]&{email?: string}}functionUserMixin<EextendsBasicTypes,BextendsConstructor<BasicHandler<E>>>(Base: B){classUserHandlerextendsBase{//constructor(...args: any[]){super(...args);}setUser(user: E["user"]){//...}}returnUserHandler;}// error TS2562: Base class expressions cannot reference class type parameters.classHandlerA<MergedTypesextendsTypesA>extendsUserMixin<MergedTypes,Constructor<BasicHandler<MergedTypes>>>(BasicHandler){// some other specific methods here}// error TS2562: Base class expressions cannot reference class type parameters.classHandlerB<MergedTypesextendsTypesB>extendsUserMixin<MergedTypes,Constructor<BasicHandler<MergedTypes>>>(BasicHandler){// some other specific methods here}typemergedTypes=TypesA&TypesBconstcurrentUser: mergedTypes["user"]={name: "Peter Parker",img: "https://....",email: "pp@example.org"}consta: HandlerA<mergedTypes>={}asany;a.setUser(currentUser)constb: HandlerB<mergedTypes>={}asany;b.setUser(currentUser);
Expected behavior:
Mixins should allow generic types and propagate them to the inherited class
Ryan Cavanaugh (@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.
Reacted by Novaal, sanperrier, Greg Kubisa, Max and Andrey Godyaev
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.
Ryan Cavanaugh (@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?
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:
functiontest<T>(argument: T): new()=>T{returnnull;}classFoo<T>extendstest<T>(/* provide an argument */){publicconstructor(){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.
Reacted by Patrick Geyer, Yichuan Shen, mat-sas-ariane-labs and John Weisz
Ryan Cavanaugh (@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.
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:
classFoo<B>extendsBaz{}
Y u do dis typscrip
Reacted by Nickolay Platonov, Benny Powers, Dylan Vorster, Ben Moon, Elies Lou ⚡️ and Philipp
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.
Yes, test() gets called only once - but that's all you want anyway. The argument that it doesn't vary based on its type doesn't mean much, because generics in typescript don't really have any effect on the generated code. They're there only for the type system.
I'm going to go further and say that I was wrong too, when I partially agreed that the parameters are presenting a problem, because now I realize that there is a possible solution.
In the example you gave:
functiontest<T>(argument: T): new()=>T{returnnull;}classFoo<T>extendstest<T>(/* provide an argument */){publicconstructor(){super();}}
What this means for Foo is that the validity of Bar is going to be checked when Foo will get declared. If Bar is a Whatever, then it passes, if not, it fails, so the arguments of the constructor signature simply place a constraint on T.
I understand that there are practical considerations here, because test() would get called regardless of the existence of any Foo instances, but if the type parameters on the constructor signature can be inferred or temporarily discarded (in the case where they're only used as a return type), then this should be theoretically implementable.
To summarize:
test() gets called only once. It doesn't vary based on its template arguments anyway.
The validity of test()'s parameters, can be verified independent of Foo, and they can definitely be passed before the declaration of any Foo instances, as long as their type doesn't depend on template parameters, or if it can be inferred.
When Foo instances are declared, test()'s parameters become constraints on what T can be.
Reacted by John Louderback, Galuma, Sebastian Nemeth and vilicvane
At the very least I would expect that declaration merging would be possible over a declared class to workaround this limitation but it isn't.
Not being able to workaround a relatively simple case as this is really 👎
// ------------------------------------ typeConstructor<T=object,Aextendsany[]=any[]>=new(...a: A)=>TfunctionFooMixin<TextendsConstructor>(Base: T){classFooextendsBase{foo='foo'}returnFooastypeofFoo&T}functionBarMixin<TextendsConstructor>(Base: T){classBarextendsFooMixin(BaseasunknownasConstructor){test(){console.log(this.foo)}}returnBarastypeofBar&T}classBase<T>{d(x: T){return2}}// ------------------------------------ reproduction// This is where the true bug lies!// declaration merging throws an error so it is impossible to workaround this limitationinterfaceA<Textendsstring>extendsBase<T>{}// T & T are unrelated so it is eq to `Base<any>`classA<Textendsstring>extendsFooMixin(Base<T>){}classNonGenericSubClassextendsBarMixin(Base<string>){}newA().d(4);// any :(console.log(newA().foo)newNonGenericSubClass().d(4);newNonGenericSubClass().d('4');
Output
"use strict";// ------------------------------------ functionFooMixin(Base){classFooextendsBase{constructor(){super(...arguments);this.foo='foo';}}returnFoo;}functionBarMixin(Base){classBarextendsFooMixin(Base){test(){console.log(this.foo);}}returnBar;}classBase{d(x){return2;}}// T & T are unrelated so it is eq to `Base<any>`classAextendsFooMixin((Base)){}classNonGenericSubClassextendsBarMixin((Base)){}newA().d(4);// any :(console.log(newA().foo);newNonGenericSubClass().d(4);newNonGenericSubClass().d('4');
Activity
RyanCavanaugh commentedon Aug 3, 2018
This is intentional. See #17829 and #17922 , especially #17829 (comment)
rzvc commentedon Aug 5, 2018
Ryan Cavanaugh (@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.
RyanCavanaugh commentedon Aug 6, 2018
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,SomeExpris only evaluated once and cannot vary overTbecause there isn't aTyet.rzvc commentedon Aug 6, 2018
Ryan Cavanaugh (@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
Tnot be passed totest<T>(), in the inheritance list of Foo, when the type checker does its thing?RyanCavanaugh commentedon Aug 6, 2018
rzvc let's say you wrote
How many times is
testinvoked?The answer is 1, because
testonly 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 typeT.So it makes no sense for
stringandnumberto be provided attest, because there's no possible way thattestis actually generic overT- its behavior cannot vary over its type parameter, because it can never deal with an actual value ofTduring its lifetime.Put another way, let's say you wrote this:
There's nothing to "fill in the blank" with -
testis lying about being generic; it doesn't actually vary over its argument because it has no arguments.rzvc commentedon Aug 7, 2018
Ryan Cavanaugh (@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.
tedchirvasiu commentedon Sep 8, 2018
What seems weird to me is that this is okay:
Passing generics to anonymous classes is also okay:
However if we try to make the anonymous class inherit from a base class received as a parameter (mixin style), it breaks:
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:
Y u do dis typscrip
Tundon commentedon Jun 7, 2019
Ryan Cavanaugh (@RyanCavanaugh)
Correct me if I am wrong, but if the similar generics code is in c++, there will definitely be two
Fooclasses, andtest()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?
Brain2000 commentedon Jul 2, 2019
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.
tannerntannern commentedon Jul 30, 2019
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.
Adds examples/tests of constructing generic types.
Adds examples/tests of constructing generic types.
Adds examples/tests of constructing generic types.
Adds examples/tests of constructing generic types.
Adds examples/tests of constructing generic types.
rzvc commentedon Feb 23, 2022
Hey guys, can we please reopen this issue? The problem still exists, and I believe the arguments made against it are wrong.
In reply to Ryan Cavanaugh (@RyanCavanaugh)'s answer:
Yes, test() gets called only once - but that's all you want anyway. The argument that it doesn't vary based on its type doesn't mean much, because generics in typescript don't really have any effect on the generated code. They're there only for the type system.
I'm going to go further and say that I was wrong too, when I partially agreed that the parameters are presenting a problem, because now I realize that there is a possible solution.
In the example you gave:
You could have the following:
What this means for Foo is that the validity of Bar is going to be checked when Foo will get declared. If Bar is a Whatever, then it passes, if not, it fails, so the arguments of the constructor signature simply place a constraint on T.
I understand that there are practical considerations here, because test() would get called regardless of the existence of any Foo instances, but if the type parameters on the constructor signature can be inferred or temporarily discarded (in the case where they're only used as a return type), then this should be theoretically implementable.
To summarize:
ShaMan123 commentedon Dec 28, 2022
At the very least I would expect that declaration merging would be possible over a declared class to workaround this limitation but it isn't.
Not being able to workaround a relatively simple case as this is really 👎
Output
Compiler Options
{ "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, "strictBindCallApply": true, "noImplicitThis": true, "noImplicitReturns": true, "alwaysStrict": true, "esModuleInterop": true, "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "target": "ES2017", "jsx": "react", "module": "ESNext", "moduleResolution": "node" } }Playground Link: Provided
force CanvasEvents spec
Improve TColumnConfig typing on columns that extend TableColumnTextBa…