Typescript: Generic `{ new(...args: any[]): T }` parameter doesn't work for abstract classes

Created on 30 Nov 2015  路  21Comments  路  Source: microsoft/TypeScript

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.

Working as Intended

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:

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.

All 21 comments

@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

  • The constrained type parameter <T extends typeof Model> should be assumed to be a concrete type, or
  • There should be a mechanism to indicate that a type is a concrete type, like <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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Roam-Cooper picture Roam-Cooper  路  3Comments

blendsdk picture blendsdk  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

Zlatkovsky picture Zlatkovsky  路  3Comments