-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Add support for abstract constructor types #36392
Conversation
CC: @weswigham, @RyanCavanaugh, @DanielRosenwasser (as suggested reviewers is not working). |
Does this also support mixins with abstract classes? Could you add a test for that, too? |
136dbf0
to
757b70f
Compare
I'm going to revert support for // [source.ts]
function MyMixin<TBaseClass extends abstract new (...args: any[]) => any>(
baseClass: TBaseClass
) {
abstract class MyMixinClass extends baseClass {
abstract abstractMethod(): void;
}
return MyMixinClass;
}
// [output.d.ts]
declare function MyMixin<TBaseClass extends abstract new (...args: any[]) => any>(
baseClass: TBaseClass
): abstract class extends TBaseClass {
abstract abstractMethod(): void;
}; |
757b70f
to
eb5789a
Compare
Build failures seem to be due to the fact that eslint isn't handling the construct signatures having modifiers in |
0b9fd25
to
65a49a7
Compare
If the constraint for It won't be helpful if we're stuck with either abstract or not-abstract. I think what people really want is that if they pass in an abstract class, then the mixin result is an abstract class (unless the mixin class implements the methods/properties), or if they pass a non-abstract class, then the mixin result is not abstract. Otherwise the usability of mixins would be inflexible. The following code has some type information omitted (like plain JS) so that I can clearly show the intention: abstract class Foo {
abstract fooMethod(): void
}
function CoolMixin(Base) {
return class Cool extends Base {
coolMethod() {}
}
}
class MyClass extends Foo {
// Required to implement the abstract fooMethod here.
// ...
}
class MyOtherClass extends CoolMixin(Foo) {
// Should also be required to implement the abstract fooMethod here.
// ...
} As a workaround, we could do the following but it is less ideal: abstract class Foo {
abstract fooMethod(): void
}
function CoolMixin(Base) {
return class Cool extends Base {
coolMethod() {}
}
}
class MyClass extends Foo {
// Required to implement the abstract fooMethod here.
// ...
}
class _MyOtherClass extends Foo {
// Required to implement the abstract fooMethod here.
}
class MyOtherClass extends CoolMixin(_MyOtherClass) {
// ...
} And if the mixin returns an abstract class, it should inherit abstractedness. So, abstract class Foo {
abstract fooMethod(): void
}
function OtherMixin(Base) {
return abstract class Other extends Base {
otherMethod() {}
}
}
class MyClass extends Foo {
// Required to implement fooMethod here.
// ...
}
class MyOtherClass extends OtherMixin(Foo) {
// Required to implement fooMethod and otherMethod here.
// ...
}
class Bar {
barMethod() {...}
}
// the previous OtherMixin application accepted an abstract class, and now this application accepts a non-abstract class:
class MyNextClass extends OtherMixin(Bar) {
// Required to implement otherMethod here, and the barMethod is inherited.
// ...
} Note that last example in particular accepts both abstract and non-abstract base classes (I didn't show an example of I believe that's what we should aim for in TypeScript, otherwise we're not effectively modeling plain JS in the most useful way. Mixins are suppose to be mixed-and-matched, but if we can not work with I'm merely a TypeScript end user, but I hope some solution can be found so that mixins are as convenient as they are in plain JS but with structural types in place. |
# Conflicts: # src/compiler/parser.ts # src/compiler/types.ts
@typescript-bot pack this |
Hey @rbuckton, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running |
@trusktr: A function Mixin<TBase extends abstract new (...args: any[]) => {}>(base: TBase) {
abstract class M extends base {
}
return M;
}
abstract class AbstractBase {
abstract m(): number;
}
class ConcreteBase {
m(): number { return 1;}
}
// error: C must implement abstract method `m`
class C extends Mixin(AbstractBase) {
}
// no error
class D extends Mixin(ConcreteBase) {
} |
Although it should probably be an error for |
@typescript-bot pack this |
I think the one thing that I'm still a little bit surprised about is that there's no way to rewrite the following example abstract class Foo {
abstract fooMethod(): void
}
function extendFoo(FooCtor: abstract new () => Foo) {
// Error! Must implement 'fooMethod'
return class extends FooCtor {
}
} in such a way that The first reason I think this is weird is because abstract class Foo {
abstract fooMethod(): void
}
// Homomorphic mapped type over `Foo`
type CopyFoo = {
[K in keyof Foo]: Foo[K]
}
function extendFoo(FooCtor: abstract new () => CopyFoo) {
// No error now!
return class extends FooCtor {
}
} The second is that in order to model this sort of pattern, you always have to declare a class, even if you won't necessarily be extending from that class (and even if nobody is going to be extending from that class). That on its own feels kind of strange in our mostly-structural type system. But I don't really know the right way to reconcile all of this - would "fixing" these issues mean that interfaces would have to allow |
The issue is that we don't (yet) have a way to represent this type in any other way. #41587 will eventually add syntax that would permit this in type-space: type TFoo = typeof abstract class {
constructor();
abstract fooMethod(): void;
};
function extendFoo(FooCtor: TFoo) { ... } However, type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any; With a new type TFooInst = InstanceType<typeof abstract class {
abstract fooMethod(): void;
}>; In the end we will need both abstract constructor types and Whether or not we eventually do something else for |
Actually, the example above isn't the problem, as function extendFoo(FooCtor: abstract new () => Foo) {
// No error
return class extends FooCtor {
fooMethod(): void {} // implements it just fine.
}
} While this isn't necessarily sound, it aligns with other inconsistencies in the language, in that we assume interface Foo {
normalMethod(): void;
}
declare function FooMixin<Ctor extends abstract new(...args: any[]) => any>(base: Ctor): Ctor & (abstract new (...args: any[]) => Foo);
class Concrete {}
class SubConcrete extends MixinFoo(Concrete) {} // ok
abstract class Abstract {
abstract fooMethod(): void;
}
class SubConcreteAbstract extends FooMixin(Abstract) {
// I must implement 'fooMethod' because it's marked 'abstract' in the base class.
fooMethod(): void {}
} |
No. We declare a // as a class
abstract class Foo {
abstract fooMethod(): void;
}
// as an interface/constructor
interface Foo {
abstract fooMethod(): void;
}
let Foo: abstract new () => Foo; I would imagine that if we did introduce |
I'll be pushing up a minor change to declaration emit/quick info shortly to handle inferred return types that result in an anonymous type with abstract construct signatures. For reference, in this case: // @target: esnext
// @declaration: true
interface Mixin {
mixinMethod(): void;
}
function Mixin<TBase extends abstract new (...args: any[]) => any>(baseClass: TBase) {
// must be `abstract` because we cannot know *all* of the possible abstract members that need to be
// implemented for this to be concrete.
abstract class MixinClass extends baseClass implements Mixin {
mixinMethod(): void {}
static staticMixinMethod(): void {}
}
return MixinClass;
} We currently emit this: declare function Mixin<TBase extends abstract new (...args: any[]) => any>(baseClass: TBase): {
new (...args: any[]): { // does not preserve `abstract`
[x: string]: any;
mixinMethod(): void;
};
staticMixinMethod(): void;
}) & TBase; When we need to emit this instead: declare function Mixin<TBase extends abstract new (...args: any[]) => any>(baseClass: TBase): ((abstract new (...args: any[]) => {
[x: string]: any;
mixinMethod(): void;
}) & {
staticMixinMethod(): void;
}) & TBase; |
@RyanCavanaugh can you take one more look with the update for declaration emit? |
d88027d
to
cfec2ca
Compare
* Add support for abstract constructor types * Add backwards-compatible overloads for creating/updating constructor types * Reverting use of 'abstract' in lib/es5.d.ts due to eslint issues * Update baseline due to reverting lib * Add error for failing to mark an mixin class as abstract * Fix declaration/quick info for abstract construct signatures
The Model/Entity being abstract caused compilation errors as TS 4.2 reinforces abstract checks. https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/ microsoft/TypeScript#36392 Signed-off-by: Raymond Feng <[email protected]>
The Model/Entity being abstract caused compilation errors as TS 4.2 reinforces abstract checks. https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/ microsoft/TypeScript#36392 Signed-off-by: Raymond Feng <[email protected]>
The Model/Entity being abstract caused compilation errors as TS 4.2 reinforces abstract checks. https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/ microsoft/TypeScript#36392 Signed-off-by: Raymond Feng <[email protected]>
Sorry to ask, but has this already been released? |
This doesn't appear to actually be present? There is no mention of |
They couldn't be added at the same time due to a lag between TS releases and the TypeScript ESLint plugin. They will be added by #43380. |
The Model/Entity being abstract caused compilation errors as TS 4.2 reinforces abstract checks. https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/ microsoft/TypeScript#36392 Signed-off-by: Raymond Feng <[email protected]>
I was a little surprised that you can't write |
It's hinted at a bit here: #36392 (comment), but was something we discussed in a Design Meeting. Adding |
The declaration emit seems to not enforce abstract members... from type Constructor = abstract new (...args: any[]) => {};
function mixin<TBase extends Constructor>(Base: TBase) {
abstract class MyClass extends Base {
abstract abs(): void;
}
return MyClass;
} I get type Constructor = abstract new (...args: any[]) => {};
declare function mixin<TBase extends Constructor>(Base: TBase): (abstract new (...args: any[]) => {
abs(): void;
}) & TBase; But if I use that as an ambient declaration (imagine publishing a package based on this). Then an implementation lacking abs doesn't error: class Broken extends mixin(class {}) {
field = 5;
} Playground (this obviously errors as it's not using ambient) |
I have noticed that abstract constructor types do not accept a class with a
What is the reason for this behavior? |
This adds support for the
abstract
keyword on constructor types and construct signatures, allowing you to indicate the signature isabstract
. This also updates the definitions forInstanceType
andConstructorParameters
to use theabstract
modifier:Syntax
Semantics
new
(this previously only applied to abstract classes)Examples - Basic Usage
Examples - Mixins
Fixes #26829
Fixes #35576