Following up on https://github.com/Microsoft/TypeScript/issues/5236#issuecomment-147738916, the proposed solution doesn't cover all bases now that abstract classes are supported.
abstract class A {}
class B extends A {}
function isType<T>(value: any, key: { new(...args: any[]): T }): boolean {
return value instanceof key;
}
console.log(isType((new B), A))
Error TS2345: Argument of type 'typeof A' is not assignable to parameter of type 'new (...args: any[]) => A'. Cannot assign an abstract constructor type to a non-abstract constructor type.
On a related note, interfaces and abstract classes feel a bit kludgey since I can't seem to verify that an arbitrary object actually conforms to them. I can require them as parameter types and use user-defined type guards, but there's no generic equivalent to instanceof
.
@RyanCavanaugh
As a guess, the error seems to suggest there is a way to denote an "abstract constructor type" (eg typeof A
), which would solve your problem. Maybe abstract new (...args:any[]):T
or something?
I interpreted the error to mean that typeof A
is an abstract constructor type. Normally (as a non-abstract type), this is fine since typeof A
is a subset of new (...args: any[]) => A
. But it looks like abstract types don't have an equivalent superset?
the error is that A
is not constructable. your function isType
says it expects things that it can construct, though in the implementation it never constructs anything. so you can not pass an abstract class to a function that may use it to construct objects. consider:
function makeInstance(constructor: new()=> T): T {
return new constructor();
}
makeInstace(B); // OK
makeInstace(A); // Error as you would expect
i think we need a way to enable the scenario, though this needs a propsal
though in the implementation it never constructs anything
We've discussed this issue elsewhere, and this is not the only thing a construct signature is used for. One specific scenario is checking super
calls as well. Any solution to this problem should enable the user to extend a given value with a subclass as well.
Right, I got burned by that a few times when learning TypeScript - this is why I only use the { new(...args: any[]): T }
syntax now. I haven't found any real uses for typeof T
, outside of strict type bounds (which I haven't encountered).
I also just ran into this when trying to find a way to use abstract classes in the context of a DI container. Something like:
registerType<T>(base: AbstractClass<T>, impl: ConcreteClass<T>);
interface ConcreteClass<T> {
new (...args: any[]): T;
}
interface AbstractClass<T> {
// ???
}
+1 for this issue, I have a real-life scenario as well where I have a function which accepts constructors but only for reflection purposes; e.g. it does instanceof checks but nothing else with the constructor, just like the above examply:
function isType<T>(value: any, key: { new(...args: any[]): T }): boolean {
return value instanceof key;
}
Currently I have to cast the type to any
when passing it to isType
. Being able to mark the constructor as (possibly) abstract
in the type would solve this issue.
^^ Same, I have a real life scenario; an Angular component factory that accepts a controller constructor as an argument, and this constructor is always a child of an abstract base.
@thorn0 suggested a very neat solution which works very well in my case, but I do consider the inability to specify an abstract with this syntax, while it compiles cleanly with interface syntax.
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11541#issuecomment-249993744
Passing a constructor reference is the only way I know to materialize a parameterized type in a function signature per issue #4890. This restriction on passing a constructor reference constrains useful type checking.
I also encounter this for a DI:
container.registerInstance(AbstractClass, someInstance)
const instance = container.get(AbstractClass)
instance === someInstance // true
// where
class Container {
get<T>(key: new (...args: any[]) => T): T
}
@unional As mentioned in my linked issue above, you can do the following and everything works as expected:
class Container {
get<T>(key: (new (...args: any[]) => T) | Function): T
}
abstract class AbstractClass {}
const instance = container.get(AbstractClass); // is of type AbstractClass
@hediet It looks like a quirk in the type inference system. If container.get(AbstractClass)
does not cause a type check error, certainly the type checking engine must be treating AbstractClass
as being of type Function
when it checks that the parameters are of the correct type. However, when it comes time to determine what T
is, the compiler must be using new (...args: any[]) => T
because it is certainly not possible to infer that T
is AbstractClass
from Function
.
But even if (surprisingly) it turns out not to be a quirk, adding Function
to the signature means that you can now call container.get(...)
with any function whatsoever. That's poking a major hole in the compile-time safety checks. For instance, container.get(isNaN)
compiles fine.
According to #13907 this behavior does not seem to be a quirk. I would interpret their answer as "Yes, you can build upon this behavior".
TypeScript's typesystem isn't sound anyways - so there are unavoidable holes. Everything else is just a compromise. I would prefer a typed container.get
that works with abstract classes (and with arbitrary functions that are not supposed to be new'ed) over an untyped container.get
where it is much more likely to get the types wrong.
My use case is mixin applied to abstract class:
type Constructor<T> = new(...args: any[]) => T;
abstract class AbstractBase {}
function Mixin<T extends Constructor<object>>(Base: T) {
return class extends Base {};
}
class Derived1 extends Mixin(AbstractBase) {} // error
class Derived2 extends Mixin(AbstractBase as Constructor<AbstractBase>) {}
Currently I need to cast AbstractBase
to Constructor<AbstractBase>
.
It would be nice to have some _possibly abstract constructor type_ or _class type_.
Here is my example with more details.
@vojtechhabarta my case exactly.
@jareguo made a compromise of not checking for new(...):...
signature, but rather for .prototype
opinions?
Just ran into this. I too would like some clarification on what @amir-arad asked regarding the use of { prototype: T }
I'm not sure I grasp what that 'means' in typescript terms.
Edit: because { prototype: T }
does not declare something that it is newable it doesn't have a constructor. I've been using typeof SomeAbstractClass
and then picking off the constrictor name - thus I can't use { prototype: T }
anyway
@AdamWillden I didn't get the explanation in the last edit, but I think I agree with the first notion.
an abstract class is a class for all intents except that you can't use it directly with new
. so it makes no sense to give it a new
call signature.
However you should be able to inherit it and call it as super()
, and right now typescript uses the new
signature as the super()
signature.
so maybe add an optional explicit super signature, and allow the new
signature to be used as a fallback?
to clarify, here's some pseudo-types following @sccolbert 's code example:
interface Class<T> {
super (...args: any[]): T; // this is the signature for calling it as `super` from a child class. if it's not declared, it's automatically inferred from the new signature
}
interface ConcreteClass<T> extends Class<T>{
new : typeof this[super];
}
one can also think of something like this:
interface FinalClass<T>{
super: never;
new : (...args: any[]): T;
}
but I really don't know how I feel about that
I encountered this very similar error (my error code is TS2684 but a nearly identical error message).
Error:(46, 3) TS2684: The 'this' context of type 'T' is not assignable to method's 'this' of type 'new () => Model
'.
Type 'typeof Model' is not assignable to type 'new () => Model'.
Cannot assign an abstract constructor type to a non-abstract constructor type.
I'm attempting to build an entity controller similar to .NET Web API. I'm making use of sequelize-typescript
as a typed entity management library. Here Model<T>
is an abstract class with a generic type parameter.
import { Model } from 'sequelize-typescript';
export class EntityController<T extends typeof Model> {
constructor(private entityType: T) {}
getAll() {
return (this.entityType).findAll(); // Error TS2684
}
}
Making a concrete type first and having T
extend that instead does not yield a compiler error.
import { Model } from 'sequelize-typescript';
export class Entity extends Model<Entity> {}
export class EntityController<T extends typeof Entity> {
constructor(private entityType: T) {}
getAll() {
return (this.entityType).findAll(); // No error
}
}
It would seem to me that either
<T extends typeof Model>
should be assumed to be a concrete type, or<T extends typeof Model is concrete>
@vojtechhabarta wrote:
type Constructor
= new(...args: any[]) => T;
class Derived1 extends Mixin(AbstractBase) {} // error
class Derived2 extends Mixin(AbstractBase as Constructor) {}
Upon receiving AbstractBase
cast to Constructor
within Mixin()
, the compiler both fails to inform me of failures to implement abstract properties of AbstractBase
and fails to require that I declare such a partially-implemented class as abstract
.
It's somewhat unfortunate that we don't have a type that means "a valid RHS to instanceof
", but Function
is quite close. The repro in the OP doesn't even need generics to do anything and loses very little expressitivity by using Function
as the second arg.
The general principle that, no, you can't construct abstract
classes remains our position.
Most helpful comment
+1 for this issue, I have a real-life scenario as well where I have a function which accepts constructors but only for reflection purposes; e.g. it does instanceof checks but nothing else with the constructor, just like the above examply:
Currently I have to cast the type to
any
when passing it toisType
. Being able to mark the constructor as (possibly)abstract
in the type would solve this issue.