Skip to content
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

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

Closed
jklmli opened this issue Nov 30, 2015 · 21 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@jklmli
Copy link

jklmli commented Nov 30, 2015

Following up on #5236 (comment), 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.

@jklmli
Copy link
Author

jklmli commented Nov 30, 2015

@RyanCavanaugh

@jesseschalken
Copy link
Contributor

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?

@jklmli
Copy link
Author

jklmli commented Nov 30, 2015

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?

@mhegazy
Copy link
Contributor

mhegazy commented Nov 30, 2015

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

@mhegazy mhegazy added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Nov 30, 2015
@DanielRosenwasser
Copy link
Member

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.

@jklmli
Copy link
Author

jklmli commented Dec 1, 2015

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).

@sccolbert
Copy link

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> {
  // ???
}

@mweststrate
Copy link

+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.

@nomaed
Copy link

nomaed commented Sep 28, 2016

^^ 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.

DefinitelyTyped/DefinitelyTyped#11541 (comment)

@karlthepagan
Copy link

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.

@unional
Copy link
Contributor

unional commented Mar 13, 2017

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
}

@hediet
Copy link
Member

hediet commented Mar 18, 2017

@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

@lddubeau
Copy link

@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.

@hediet
Copy link
Member

hediet commented Mar 30, 2017

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.

@vojtechhabarta
Copy link

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.

@amir-arad
Copy link

amir-arad commented Jul 16, 2017

@vojtechhabarta my case exactly.
@jareguo made a compromise of not checking for new(...):... signature, but rather for .prototype
opinions?

@AdamWillden
Copy link

AdamWillden commented Jul 22, 2017

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

@amir-arad
Copy link

amir-arad commented Jul 23, 2017

@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

@matthewstrom
Copy link

matthewstrom commented Nov 30, 2017

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>

@jtlapp
Copy link

jtlapp commented Jan 29, 2018

@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.

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Feb 6, 2018
@RyanCavanaugh
Copy link
Member

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.

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests