-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Mixin does not allow Generic #26154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This is intentional. See #17829 and #17922 , especially #17829 (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. |
@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 |
@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 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 The answer is 1, because This is different from So it makes no sense for 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 - |
@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 |
Correct me if I am wrong, but if the similar generics code is in c++, there will definitely be two |
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. |
This mostly works as expected, with the one caveat that constructing a generic `Foo<T>` with `ctor.new(Foo)` actually results in a return-only generic. TypeScript is a little awkward about these and it will actually be typed as `Foo<unknown>`. This is unfortunate, and the best practice around return-only generics is generally considered casting them to the desired type at each call site. This means that a user should really do `ctor.new(Foo) as ctor<Foo<T>>`, which is a bit annoying syntatically, but not that big a deal in the grand scheme of things. More annoyingly is that TypeScript does not allow type parameters in the `extends` clause of a class when used as an expression. This makes sense for TypeScript because a class type `T` is not known until the class is instantiated, long after it was declared as extending a different class. However, with the recent changes in `ctor<T>`, the inheritance hierarchy is not built until construction-time, so we actually do know the value of `T`. Unfortunately this can't be expressed in TypeScript, so our best option is to simply `@ts-ignore` the error. This is not ideal, but TypeScript *mostly* does what we want in this case. The one issue here is that superclass members are typed as `any`, which is quite unfortunate but I don't see a good way around that. See: microsoft/TypeScript#26154 (comment)
This mostly works as expected, with the one caveat that constructing a generic `Foo<T>` with `ctor.new(Foo)` actually results in a return-only generic. TypeScript is a little awkward about these and it will actually be typed as `Foo<unknown>`. This is unfortunate, and the best practice around return-only generics is generally considered casting them to the desired type at each call site. This means that a user should really do `ctor.new(Foo) as ctor<Foo<T>>`, which is a bit annoying syntatically, but not that big a deal in the grand scheme of things. More annoyingly is that TypeScript does not allow type parameters in the `extends` clause of a class when used as an expression. This makes sense for TypeScript because a class type `T` is not known until the class is instantiated, long after it was declared as extending a different class. However, with the recent changes in `ctor<T>`, the inheritance hierarchy is not built until construction-time, so we actually do know the value of `T`. Unfortunately this can't be expressed in TypeScript, so our best option is to simply `@ts-ignore` the error. This is not ideal, but TypeScript *mostly* does what we want in this case. The one issue here is that superclass members are typed as `any`, which is quite unfortunate but I don't see a good way around that. See: microsoft/TypeScript#26154 (comment)
This mostly works as expected, with the one caveat that constructing a generic `Foo<T>` with `ctor.new(Foo)` actually results in a return-only generic. TypeScript is a little awkward about these and it will actually be typed as `Foo<unknown>`. This is unfortunate, and the best practice around return-only generics is generally considered casting them to the desired type at each call site. This means that a user should really do `ctor.new(Foo) as ctor<Foo<T>>`, which is a bit annoying syntatically, but not that big a deal in the grand scheme of things. More annoyingly is that TypeScript does not allow type parameters in the `extends` clause of a class when used as an expression. This makes sense for TypeScript because a class type `T` is not known until the class is instantiated, long after it was declared as extending a different class. However, with the recent changes in `ctor<T>`, the inheritance hierarchy is not built until construction-time, so we actually do know the value of `T`. Unfortunately this can't be expressed in TypeScript, so our best option is to simply `@ts-ignore` the error. This is not ideal, but TypeScript *mostly* does what we want in this case. The one issue here is that superclass members are typed as `any`, which is quite unfortunate but I don't see a good way around that. See: microsoft/TypeScript#26154 (comment)
This mostly works as expected, with the one caveat that constructing a generic `Foo<T>` with `ctor.new(Foo)` actually results in a return-only generic. TypeScript is a little awkward about these and it will actually be typed as `Foo<unknown>`. This is unfortunate, and the best practice around return-only generics is generally considered casting them to the desired type at each call site. This means that a user should really do `ctor.new(Foo) as ctor<Foo<T>>`, which is a bit annoying syntatically, but not that big a deal in the grand scheme of things. More annoyingly is that TypeScript does not allow type parameters in the `extends` clause of a class when used as an expression. This makes sense for TypeScript because a class type `T` is not known until the class is instantiated, long after it was declared as extending a different class. However, with the recent changes in `ctor<T>`, the inheritance hierarchy is not built until construction-time, so we actually do know the value of `T`. Unfortunately this can't be expressed in TypeScript, so our best option is to simply `@ts-ignore` the error. This is not ideal, but TypeScript *mostly* does what we want in this case. The one issue here is that superclass members are typed as `any`, which is quite unfortunate but I don't see a good way around that. See: microsoft/TypeScript#26154 (comment)
This mostly works as expected, with the one caveat that constructing a generic `Foo<T>` with `ctor.new(Foo)` actually results in a return-only generic. TypeScript is a little awkward about these and it will actually be typed as `Foo<unknown>`. This is unfortunate, and the best practice around return-only generics is generally considered casting them to the desired type at each call site. This means that a user should really do `ctor.new(Foo) as ctor<Foo<T>>`, which is a bit annoying syntatically, but not that big a deal in the grand scheme of things. More annoyingly is that TypeScript does not allow type parameters in the `extends` clause of a class when used as an expression. This makes sense for TypeScript because a class type `T` is not known until the class is instantiated, long after it was declared as extending a different class. However, with the recent changes in `ctor<T>`, the inheritance hierarchy is not built until construction-time, so we actually do know the value of `T`. Unfortunately this can't be expressed in TypeScript, so our best option is to simply `@ts-ignore` the error. This is not ideal, but TypeScript *mostly* does what we want in this case. The one issue here is that superclass members are typed as `any`, which is quite unfortunate but I don't see a good way around that. See: microsoft/TypeScript#26154 (comment)
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 @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: function test<T>(argument: T): new () => T
{
return null;
}
class Foo<T> extends test<T>(/* provide an argument */)
{
public constructor()
{
super();
}
} You could have the following: class Foo<T> extends test<T>(Bar)
{
public constructor()
{
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:
|
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. // ------------------------------------
type Constructor<T = object, A extends any[] = any[]> = new (...a: A) => T
function FooMixin<T extends Constructor>(Base: T) {
class Foo extends Base {
foo = 'foo'
}
return Foo as typeof Foo & T
}
function BarMixin<T extends Constructor>(Base: T) {
class Bar extends FooMixin(Base as unknown as Constructor) {
test() {
console.log(this.foo)
}
}
return Bar as typeof Bar & T
}
class Base<T> {
d(x: T){
return 2
}
}
// ------------------------------------ reproduction
// This is where the true bug lies!
// declaration merging throws an error so it is impossible to workaround this limitation
interface A<T extends string> extends Base<T>{
}
// T & T are unrelated so it is eq to `Base<any>`
class A<T extends string> extends FooMixin(Base<T>) {
}
class NonGenericSubClass extends BarMixin(Base<string>) {
}
new A().d(4); // any :(
console.log(new A().foo)
new NonGenericSubClass().d(4);
new NonGenericSubClass().d('4'); Output"use strict";
// ------------------------------------
function FooMixin(Base) {
class Foo extends Base {
constructor() {
super(...arguments);
this.foo = 'foo';
}
}
return Foo;
}
function BarMixin(Base) {
class Bar extends FooMixin(Base) {
test() {
console.log(this.foo);
}
}
return Bar;
}
class Base {
d(x) {
return 2;
}
}
// T & T are unrelated so it is eq to `Base<any>`
class A extends FooMixin((Base)) {
}
class NonGenericSubClass extends BarMixin((Base)) {
}
new A().d(4); // any :(
console.log(new A().foo);
new NonGenericSubClass().d(4);
new NonGenericSubClass().d('4'); 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 |
…se (#1997) # Pull Request ## 🤨 Rationale Resolves #1964 ## 👩💻 Implementation `TableColumnTextBase` cannot simply be made generic because TypeScript doesn't support generics and mixins together: microsoft/TypeScript#26154. Therefore, each concrete instance of `TableColumnTextBase` should add its own mixins in a way that they get mixed into `TableColumnTextBase<TColumnConfig>` rather than `TableColumn`. To avoid code duplicate, I created a function, `mixinTextBase`, that mixes in the appropriate mixins for `TableColumnTextBase`. ## 🧪 Testing - Unit tests still pass - Manually verified that the type of `this.columnInternals.columnConfig` is correct for a column using `mixinTextBase` - Sanity tested examples in storybook to make sure columns still behave as expected ## ✅ Checklist <!--- Review the list and put an x in the boxes that apply or ~~strike through~~ around items that don't (along with an explanation). --> - [ ] I have updated the project documentation to reflect my changes or determined no changes are needed.
TypeScript Version: 3.1.0-dev.20180802
Search Terms:
Code
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:
The text was updated successfully, but these errors were encountered: