TypeScript Version: 3.4.0-dev.20190130
Search Terms: abstract mixin
When using a mixin function, if the constraint to the constructor returns an abstract class (T extends new (...a: any[])=> AbBase) we are forced to implement the abstract members in the mixin class. This seems strange since if the passed in class satisfies the constructor signature constraint it will not be an abstract class, as abstract classes do not have a callable constructor.
Code
abstract class AbBase {
abstract m(): void ;
}
function mixin<T extends new (...a: any[])=> AbBase>(c: T) {
let a = new c(); // We can instantiate
return class extends c { // but when deriving the class we need to redefine abstract members
}
}
mixin(AbBase) //Argument of type 'typeof AbBase' is not assignable to parameter of type 'new (...a: any[]) => AbBase' as expected, no chance c would be abstract
Expected behavior:
The mixin function should compile without error
Actual behavior:
The compiler raises an error on the class defined in the mixin function : Non-abstract class expression does not implement inherited abstract member 'm' from class 'AbBase'.
Playground Link: Link
Related Issues: None
@rbuckton FYI, I found this #26617 related issue. This issue seems more general. If we have a constructor signature returning an abstract class the compiler will complain we have to implement all members even though we can never assign an abstract class to such a constructor signature
abstract class Base {
abstract m(): void
}
let ctor: new () => Base;
ctor = Base /// error we can't assign an abstract class to this
ctor = class extends Base { // we can only assign a non-abstract class
m() { }
}
// Non-abstract class 'Derived' does not implement inherited abstract member 'm' from class 'Base'
// Why is this an error at all ? The constructor returns an instance of an abstract class but can never be an abstract class
class Derived extends ctor {
}
IMO the true issue is there is no way to represent an abstract class constructor. Ideally I would like to be able to tell the compiler that I have an abstract class constructor and it should enforce implementation of abstract members (something like new abstract () => Base). If my constructor signature is just new () => Base then this is not the constructor of an abstract class and I should not have to implement the methods
let ctor: new abstract () => Base; // new syntax, abstract new () => Base also works, but new abstract () => Base seems easier to implement
ctor = Base /// this is allowed now
ctor = class extends Base { // can be allowed, consumers of ctor will have to reimplement m, but no harm will come of it.
m() { }
}
new ctor() // should be an error
class Derived extends ctor { // errors expected here we don't implement base members
}
This can make mixins a PITA!
It would be really useful: yesterday I wanted to make a mixin, and constrain it to classes of a certain base type (which happens to be marked as abstract). It doesn't make sense for the mixin to implement all of the abstract members. The mixin just wants to enforce a constraint, and to even possibly call abstract methods, under the assumption that the user of the mixin (and of the abstract base class) will be implementing the members.
Here's a playground example.
Is there some workaround, some way to remove the abstracted (and abstracted in some type functions to make it clean), so that in the mixin the constrained base class does not appear to be abstract?
I've tried some tricks like
type NonAbstractBase = Pick<Base, keyof Base>
and then using NonAbstractBase for the constraint type, but that has issues where it converts method types into property types, etc, and then that introduces other errors.
Another workaround is to just not use the abstract keyword, and do stuff like throw new Error('Method not implemented') in the base class, delegating the check to runtime. But doing this makes it easy to make mixins.
It would be super sweet to be able to do this in TypeScript, just like in plain JS.
Looks like we need a way to pass along abstractedness (f.e. forward abstractedness to the mixin return type).
(Relating to abstract support for mixins, there's also https://github.com/microsoft/TypeScript/issues/32122)
@dragomirtitian I think the solution to this is actually simple. The abstract keyword simply needs to be usable anywhere besides at the top level.
We should be able to stick the abstract keyword in front of the class that a mixin returns, just in this example: playground.
Secondly, regarding your point, I don't think we necessarily need a way to specify an abstract new() => ... constructor. I think the type check should allow something like new(...args: any[]) => SomeAbstractClass to be a valid type (because, it is representing a totally valid JS object), _but_ type type checker should forbid the use of new on callsites using new with that type.
So, this could be the case:
abstract class SomeAbstractClass {/* ... */}
const Ctor = new(...args: any[]) => SomeAbstractClass // NO ERROR
new Ctor // ERROR, can't call `new` on abstract class.
If the type checker did all of my above comment, then:
abstract class {} syntax is already perfectly valid existing syntax. It would make perfect sense to be able to return an abstract class from a class factory function.I believe this would be the most intuitive way to solve the issue without introducing new syntax.
@trusktr I don't necessarily agree that calling new on new(...args: any[]) => SomeAbstractClass should issue an error. The constructor signature does not mean that the constructor returns that exact type, but rather any derived type. This is very useful where you want to have a collection of constructors all returning a common base type.
interface X { m(): void; }
class A implements X { m() { console.log("A");} }
class B implements X { m() { console.log("B");} }
class C implements X { m() { console.log("C");} }
let o : Array<new () => X> = [A, B, C];
let r = Math.round(Math.random() * 2);
new o[r]().m();
A constructor signature can return any type, be it an interface, a union whatever else, and no assumptions are made about the constructor itself based on the return type. It is only in the special case of inheritance where the fact that a constructor returns an abstract class that the assumption is made that this means the constructor itself is of an abstract class.
The constructor signature does not mean that the constructor returns that exact type
Ah, that's true. It returns any subtype. (Hence why it's better to use new (...) => {} in the mixin contraint instead of new (...) => any)
I was trying to constrain a mixin Base class to something like C extends typeof AbstractClass, but that doesn't work. Why doesn't that work? Isn't typeof AbstractClass a constructor? It seems like it would be intuitive to write it that way, and that the abstractedness of typeof AbstractClass should just be present (with the goal of not adding new syntax).
It is only in the special case of inheritance where the fact that a constructor returns an abstract class that the assumption is made that this means the constructor itself is of an abstract class.
This is important. I figured how to get rid of the issue with abstract class being returned by just not returning it all in the same statement (same applies to decorators):
abstract class SomeBaseClass {
abstract base(): boolean
}
function CoolMixin<
C extends new(...args: any[]) => SomeBaseClass,
T extends InstanceType<C>
>(Base: C) {
abstract class Cool extends Base {
cool() {
console.log("cool");
}
};
return Cool
}
class Bar extends CoolMixin(SomeBaseClass) { // ERROR
bar() {
console.log("bar");
}
}
Seems like that's just a bug.
In my previous example, at a conceptual level, how is SomeBaseClass (despite being abstract) not a new(...args: any[]) => SomeBaseClass? Could we do it that way? It's valid JavaScript.
EDIT: Well, I guess that would be a breaking change for existing code bases, so maybe that isn't feasible.
With your idea, abstract new(...args: any[]) => Type seems to be a superset of new(...args: any[]) => Type because new(...args: any[]) => Type is assignable to a variable of type abstract new(...args: any[]) => Type.
What if they are exclusive, one not assignable to the other and vice versa?
With your abstract new idea, how does abstract new () => Type specify which members are abstract? It doesn't seem to carry that information over. Or does it?
Hmmmm, maybe we need something like abstract interface:
abstract interface Type {
nonAbstractMethod(): boolean
abstract method(): number
}
abstract interface TypeCtor {
new (): Type
}
or something
And, to tie it all together, typeof AbstractClass would effectively return something like that abstract interface TypeCtor interface, and it could be used in the constraint of a mixin.
An idea: why not have abstractedness be automatically inherited by a subclass if the subclass does not implement all the abstract features, and then throw a type error like "Can not instantiate abstract class" at the sites where the constructor is called?
Yes, this would make the type error once-removed from the place where the issue is, but maybe abstractedness inheritance would actually be a feature.
Example:
abstract Foo {
abstract foo(): number
}
// this class is abstract, because it didn't implement foo():
class Bar extends Foo {
bar = 123
}
const b = new Bar() // Error, can not instantiate Bar because it is an abstract class
Or, maybe, just simply allow abstract class {} in expression sites.
It seems simple to allow abstract classes to work in expressions. I haven't made changes to TypeScript source before. Would this be easy? It seems like a useful addition.
Hmmmm, maybe we need something like
abstract interface:abstract interface Type { nonAbstractMethod(): boolean abstract method(): number } abstract interface TypeCtor { new (): Type }or something
@trusktr Actually, if you add //@ts-ignore to the abstract methods of an interface, you'll found that TypeScript already support abstract interface members.
interface Mixin
{
//@ts-ignore
abstract abstractMethod(): void;
}
interface MixinStatic<T extends Class>
{
new(): InstanceType<T> & Mixin;
}
@JasonHK I would not recommend using @ts-ignore on anything as part of a recommended workflow. If you have to suppress an error to get the behavior you want it is by definition not sported , even if it by accident works as you are essentially relying on undocumented behavior .
@JasonHK Huh! Neat hack! I'll have to take that for a spin. I'm more interested in my consumers getting the desired features than in me avoiding mistakes (and in this case it is only a type definition that ts-ignore is applied to). At least it isn't as bad as simply reverting all the way back to plain JavaScript.
Is there an update on this? I am also failing to apply a mixin to an abstract base class and get the following error:
Cannot assign an abstract constructor type to a non-abstract constructor type. (2345)
Same construction as in https://github.com/microsoft/TypeScript/issues/29653#issuecomment-526359121
Most helpful comment
@JasonHK I would not recommend using
@ts-ignoreon anything as part of a recommended workflow. If you have to suppress an error to get the behavior you want it is by definition not sported , even if it by accident works as you are essentially relying on undocumented behavior .