TypeScript Version: 2.4.0
It would be interesting to allow type hinting for class type.
Something like
function generateMyClass(myClass: class) {
return new myClass();
}
class Foo {}
generateMyClass(Foo); // Should work
generateMyClass('something else'); // should NOT work
Currently, the closest we have is
type Instantiable = {new(...args: any[]): any};
function doSomethingWithMyClass(myClass: Instantiable ) {
// some code that doesn't matter
}
class Foo {}
abstract class Bar {}
doSomethingWithMyClass(Foo); // It works :)
doSomethingWithMyClass('something else' ); // It doesn't work and it's OK
doSomethingWithMyClass(Bar); // It doesn't work and it's NOT OK
It is of course logical that the abstract class causes an error with the code above, this is why it could be nice to have something to handle this case.
To go further, we can think about adding some genericity on it.
// With the actual implementation
type Instantiable<T = any> = {new(...args: any[]): T};
function foo<T>(myClass: Instantiable<T>): T { /* ... */ }
// With the new class type hint
function foo<T>(myClass: class<T>): T { /* ... */ }
We can actually get a little bit closer than this, but it's still lacking.
type Abstract<T> = Function & {prototype: T};
type Constructor<T> = new (...args: any[]) => T;
type Class<T> = Abstract<T> | Constructor<T>;
This can work okay for some APIs, but also accepts functions, as their prototype has type any. The type is also not considered constructable, so we can't extend from an object of type Class<T>.
I've actually just found another trick playing around with this now. I haven't seen this anywhere before, I don't know if this really works in the general case, would be nice to get some feedback.
abstract class EmptyAbstractClass {}
type Class<T = {}> = typeof EmptyAbstractClass & {prototype: T};
This seems to work nicely.
abstract class Foo {}
class Bar extends Foo {}
function foo<T>(C: Class<T>) {} // Accepts any class
foo(Foo); // Works as it should
foo(Bar); // Works as it should
foo(class{}); // Works as it should
foo(function(){}); // Fails as it should
And you can restrict the types of the class just fine.
abstract class Base { isBase = true; }
class Child extends Base {}
function bar<T extends Base>(C: Class<T>) {} // Accepts any class that extends Base
bar(Base); // Works as it should
bar(Child); // Works as it should
bar(class{}); // Fails as it should
bar(function(){}); // Fails as it should
Class<T> _is_ considered a constructable type, so we can extend from it.
function makeDerived(C: Class<{}>) {
return class extends C {};
}
However, this doesn't quite seem to work generically. This complains that type 'makeDerivedGeneric<any>.(Anonymous class)' is not assignable to type 'T'. I'm not completely sure what's going on here, or why TS can't tell that the anonymous class type extends T. Adding generic constraints doesn't seem to help.
function makeDerivedGeneric<T>(C: Class<T>) {
return class extends C {}; // Error here
}
All this is in 2.4.2.
There's an issue with @TheOtherSamP 's example: arrays.
class Foo{
public bar: string;
}
function getData<T>(clazz: Class<T>):T{
//Whatever implementation, the signature is the problem.
}
const instance: Foo = getData<Foo>(Foo);// Works as it should.
const array1: Foo[] = getData<Foo[]>(Foo);// Fails because Foo is not Foo[]'s class.
const array2: Foo[] = getData<Array<Foo>>(Array<Foo>);// Fails because type Foo[] provides no match for the signature 'new (...args: any[]): Foo[]'
@Supamiu
The issue in your example does not come from @TheOtherSamP proposal, it's just that you don't follow your function signature.
@noemi-salaun Yes and that's the issue because there's no valid signature for this type of call.
Actually, there's no type to say 'I want an array of this class', the only way to do this is:
function getArray<T>(clazz: Class<T>): T[]{
return [].push(new clazz()); //This is a bad implementation bus just giving an example here.
}
// And then we make the call for an explicit 'getArray' function, there's no way to include this in a more generic function.
const array: Foo[] = getArray<Foo>(Foo);
Because Foo[] is a valid type, there should be a way to say 'this is the class of the array type'.
I don't know if this is clear because I'm struggling to explain it.
@Supamiu
I'll just start off by saying that I still support some form of this request. Even if that trick that I found could be made to work for all scenarios, it's a bit of an ugly hack, and hardly something you could expect anyone picking up TS to get intuitively. It's a pretty common scenario, and language support for it would be great.
To be honest, I'm not entirely sure what you're saying with this example. That getArray function looks like the right way to do that to me. I'm not sure that this proposal would allow you to do that in any other way, can you provide an example of how you'd like this to work under the proposal?
Just to clear up what the class type in this proposal would actually do, my understanding is that it basically means any 'constructor function type', that is, any type that it's valid to extend from. Is that your intention @noemi-salaun?
Yes that's it. class, abstract class but not interface or type.
@noemi-salaun That sounds good. I wonder if it would perhaps make sense to express this concept slightly differently. Would allowing the abstract keyword on constructor definitions be a good way to do things?
type AbstractConstructable<T> = abstract new(...args: any[]) => T;
A concrete type would still be valid here, it wouldn't force abstractness, but allow it.
I think this fits fairly nicely with the rest of the language, and allows this to be worked fairly nicely into interfaces. This would also allow you to specify the parameters of the abstract constructor.
More involved example:
// A variable of this type is valid to extend from.
export interface FooConstructor {
abstract new(dependency: number): Foo;
readonly defaultFoo: Foo;
}
export interface Foo {}
abstract class FooInternal implements Foo {
static readonly defaultFoo: Foo;
constructor(dependency: number) {}
}
The point of course being that you could extend from a variable of type FooConstructor.
@TheOtherSamP Sorry for the delay of my answer, I think that the best way to show you an example of what I'm trying to explain is to show you the issue in actual code implementation.
This test : https://github.com/kaiu-lab/serializer/blob/proper-type-matching/test/serializer.spec.ts#L223 is failing to compile because Foo's constructor doesn't return a Foo[].
Error:(233, 16) TS2345:Argument of type 'typeof Foo' is not assignable to parameter of type 'Class<Foo[]>'.
Type 'typeof Foo' is not assignable to type 'Instantiable<Foo[]>'.
Type 'Foo' is not assignable to type 'Foo[]'.
Property 'length' is missing in type 'Foo'.
Here is the signature of the method involved (deserialize):
public deserialize<T>(obj: any, clazz?: Class<T>): T
Where Class<T> is the one you provided in your first comment.
What is missing here is the ability to provide a constructor for an array type, because Array<Foo> doesn't work neither, the error being:
Error:(233, 16) TS2345:Argument of type 'Foo[]' is not assignable to parameter of type 'Class<Foo[]>'.
Type 'Foo[]' is not assignable to type 'Instantiable<Foo[]>'.
Type 'Foo[]' provides no match for the signature 'new (...args: any[]): Foo[]'.
If we had such functionality, I'd think about something like Array<Foo> being understood as the Foo[]class, because is this case, we need to know what class to instantiate for each member of the array with the same method we're using for standard objects.
N.B: I'm starting to think that this is a complete different issue... I'll probably check if an issue about this exists and create one if it doesn't.
@SamPruden is there still no workaround for the generic case from your example above:
function makeDerivedGeneric<T>(C: Class<T>) {
return class extends C {}; // Error here
}
@mshoho as @mhegazy said in https://github.com/Microsoft/TypeScript/issues/8853#issuecomment-224398399
this is behaving as i would expect. you can only extend object types, in other words something that of a known structure. type arguments are not. the extend relationship involves checking that the structure of the derived class matches that of the base. without knowing what is the shape of the base, the compiler can not report these errors.
So it seems that makeDerivedGeneric makes no sense.
TypeScript Version: 2.4.0
It would be interesting to allow type hinting for class type.
Something like
function generateMyClass(myClass: class) { return new myClass(); } class Foo {} generateMyClass(Foo); // Should work generateMyClass('something else'); // should NOT workCurrently, the closest we have is
type Instantiable = {new(...args: any[]): any}; function doSomethingWithMyClass(myClass: Instantiable ) { // some code that doesn't matter } class Foo {} abstract class Bar {} doSomethingWithMyClass(Foo); // It works :) doSomethingWithMyClass('something else' ); // It doesn't work and it's OK doSomethingWithMyClass(Bar); // It doesn't work and it's NOT OKIt is of course logical that the abstract class causes an error with the code above, this is why it could be nice to have something to handle this case.
To go further, we can think about adding some genericity on it.
// With the actual implementation type Instantiable<T = any> = {new(...args: any[]): T}; function foo<T>(myClass: Instantiable<T>): T { /* ... */ } // With the new class type hint function foo<T>(myClass: class<T>): T { /* ... */ }
Thank you for your post @noemi-salaun! Now I know I need to wait few years to come back to typescript. The workaround solutions below are awkward.
Most helpful comment
We can actually get a little bit closer than this, but it's still lacking.
This can work okay for some APIs, but also accepts functions, as their
prototypehas typeany. The type is also not considered constructable, so we can't extend from an object of typeClass<T>.I've actually just found another trick playing around with this now. I haven't seen this anywhere before, I don't know if this really works in the general case, would be nice to get some feedback.
This seems to work nicely.
And you can restrict the types of the class just fine.
Class<T>_is_ considered a constructable type, so we can extend from it.However, this doesn't quite seem to work generically. This complains that
type 'makeDerivedGeneric<any>.(Anonymous class)' is not assignable to type 'T'. I'm not completely sure what's going on here, or why TS can't tell that the anonymous class type extends T. Adding generic constraints doesn't seem to help.All this is in 2.4.2.