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

Support for a built-in "constructable" type or "class type" #17572

Open
noemi-salaun opened this issue Aug 2, 2017 · 13 comments
Open

Support for a built-in "constructable" type or "class type" #17572

noemi-salaun opened this issue Aug 2, 2017 · 13 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@noemi-salaun
Copy link

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 { /* ... */ }
@DanielRosenwasser DanielRosenwasser added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Aug 2, 2017
@DanielRosenwasser DanielRosenwasser changed the title Support type hinting for class type Support for a built-in "constructable" type or "class type" Aug 2, 2017
@SamPruden
Copy link

SamPruden commented Aug 3, 2017

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.

@Supamiu
Copy link

Supamiu commented Aug 4, 2017

There's an issue with @TheOtherSamP 's example: arrays.

Example

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[]'

@noemi-salaun
Copy link
Author

@Supamiu
The issue in your example does not come from @TheOtherSamP proposal, it's just that you don't follow your function signature.

@Supamiu
Copy link

Supamiu commented Aug 4, 2017

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

@SamPruden
Copy link

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

@SamPruden
Copy link

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?

@noemi-salaun
Copy link
Author

Yes that's it. class, abstract class but not interface or type.

@SamPruden
Copy link

SamPruden commented Aug 4, 2017

@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) {}
}

@SamPruden
Copy link

The point of course being that you could extend from a variable of type FooConstructor.

@Supamiu
Copy link

Supamiu commented Aug 7, 2017

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

@mshoho
Copy link
Member

mshoho commented Jul 13, 2018

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

@eternalphane
Copy link

@mshoho as @mhegazy said in #8853 (comment)

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.

@manigolara
Copy link

manigolara commented Nov 30, 2020

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 { /* ... */ }

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.

romaricpascal added a commit to alphagov/govuk-design-system that referenced this issue Apr 23, 2024
First things first, this code has not even been run, it's only a draft of an API I think could be nice for initialising components, accounting for the nuances found in the different components of the Design System.

- Some components run on a specific element, some not. So the API need to allow both.
- When initialised on a specific element, the module name could be a static property of the component, however, it's probably useful to let it be set explicitely
- Further selection, like finding the  for the Copy, should probably be part of the component's responsibility, rather than the initialisation code
- Similarly, if a component needs to be initialised only once, this could be part of the component's responsibility, rather thatn the initialisation loop.

TypeScript might get in the way of having something so dynamic. Especially, I'm not 100% sure how we can note that a function accepts a 'class' as parameter. Seems it's a bit tricky based on this: microsoft/TypeScript#17572
romaricpascal added a commit to alphagov/govuk-design-system that referenced this issue Apr 23, 2024
First things first, this code has not even been run, it's only a draft of an API I think could be nice for initialising components, accounting for the nuances found in the different components of the Design System.

- Some components run on a specific element, some not. So the API need to allow both.
- When initialised on a specific element, the module name could be a static property of the component, however, it's probably useful to let it be set explicitely
- Further selection, like finding the  for the Copy, should probably be part of the component's responsibility, rather than the initialisation code
- Similarly, if a component needs to be initialised only once, this could be part of the component's responsibility, rather thatn the initialisation loop.

TypeScript might get in the way of having something so dynamic. Especially, I'm not 100% sure how we can note that a function accepts a 'class' as parameter. Seems it's a bit tricky based on this: microsoft/TypeScript#17572
36degrees added a commit to alphagov/govuk-frontend that referenced this issue May 8, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572
romaricpascal pushed a commit to alphagov/govuk-frontend that referenced this issue May 9, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <[email protected]>
romaricpascal pushed a commit to alphagov/govuk-frontend that referenced this issue May 9, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <[email protected]>
romaricpascal pushed a commit to alphagov/govuk-frontend that referenced this issue May 9, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <[email protected]>
romaricpascal pushed a commit to alphagov/govuk-frontend that referenced this issue May 10, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <[email protected]>
36degrees added a commit to alphagov/govuk-frontend that referenced this issue May 10, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <[email protected]>
36degrees added a commit to alphagov/govuk-frontend that referenced this issue May 15, 2024
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants