Skip to content

Suggestion: typeof of generic type #204

Closed

Description

@omidkrad

This issue was previously reported as bug at codeplex, now trying to submit as suggestion.

Below, the Collection.model field is supposed to accept type of TModel rather than instance of TModel:

class Model { }
class Collection<TModel extends Model> {
    model: typeof TModel; //Error: Could not find symbold 'TModel'.
    models: TModel[];
}

class MyModel extends Model { }
class MyCollection extends Collection<MyModel> {
    // This is what we want
    model: typeof MyModel = MyModel;

    // This is what we don't want
    //model: MyModel = new MyModel;
}

var coll = new MyCollection();
var modelType = coll.model;
var newModel = new modelType();

Use of typeof keyword seems to be appropriate here, but compiler doesn't understand typeof of generic type. If we said model: typeof Model; it would compile but that is not exactly what we want.

Some of the workarounds we found are the followings:

class Model { }
class Collection<TModel extends Model> {
        model: { new(): TModel; };
        models: TModel[];
}

class MyModel extends Model { }
class MyCollection extends Collection<MyModel> { }

and

class Model { }
class Collection<TModelConstructor extends typeof Model, TModel extends Model> {
        model: TModelConstructor;
        models: TModel[];
}

class MyModel extends Model { }
class MyCollection extends Collection<typeof MyModel, MyModel> { }

Feature suggestion is to allow typeof generic type.

Activity

  1. RyanCavanaugh commented on Jul 23, 2014

    @RyanCavanaugh
    Member

    typeof takes a value and produces the type that the value has. Generic type parameters are types; they are already types and there's no need to have an operator to convert them to types.

    Your workarounds are actually the correct design because they model the constraint you have -- a zero-argument constructor function for the given type.

    See also https://typescript.codeplex.com/discussions/456796

  2. omidkrad commented on Jul 23, 2014

    @omidkrad
    Author

    The link was very informative. Thank you.

  3. AlicanC commented on Jul 17, 2015

    @AlicanC

    The first workaround actually causes problems with statics:

    class Model {
      static doStuff() { }
    }
    class Collection<TModel extends Model> {
      // With this workaround, you also have to hack in doStuff() here which causes a lot of duplication.
      model: { new(): TModel; };
      models: TModel[];
    
      doStuffToModel() {
        // This is okay because we hacked in "new(): TModel"
        var model = new this.model();
    
        // This is not okay because we didn't hack in "doStuff(): void"
        this.model.doStuff();
      }
    }

    The second workaround is a lot better but also problematic:

    class Model { }
    class Collection<TModelConstructor extends typeof Model, TModel extends Model> {
      model: TModelConstructor;
      models: TModel[];
    
      static createCoolInstance<TModelConstructor extends typeof Model, TModel extends Model>(ctor: TModelConstructor): TModel {
    
        // "Type 'Model' is not assignable to type 'TModel'." so you have to "<TModel>new ctor();" which seems pretty unnecessary
        return new ctor();
    
      }
    }

    Let me explain what I think about this with some pseudo code:

    class MyClass {
      static foo() {};
      bar() {};
    }
    
    // That does something like:
    type MyClassInstanceType = { bar: () => void };
    
    type MyClassConstructorType = { new(): MyClassInstanceType; foo(): void };
    
    var MyClassValue = function() {};
    MyClassValue.prototype.bar = function () {};
    MyClassValue.foo = function () {};
    
    // So if typeof was a function, it would be something like:
    function typeof(classValue: Value): Type {
    
      return ClassValueToConstructorTypeMap[classValue];
    
    }

    My question is, why does typeof take a value? Here is an example:

    var inst: MyClass = new MyClass();
    var ctor: typeof MyClass = MyClass;

    The first "MyClass" refers to MyClassInstanceType and the second one refers to MyClassValue.
    On the second line, couldn't the first "MyClass" refer to MyClassInstanceType again and get MyClassConstructorType from that?
    If changing typeof to or overloading it with "InstanceType => ConstructorType" is breaking, I think we could easily have another operator for it. (constructorof? staticof?)

  4. AlicanC commented on Jul 17, 2015

    @AlicanC

    And some examples would be:

    autoConstructor<T>(ctor: constructorof T): T {
    
      return new ctor();
    
    }
    
    var inst;
    
    /* Works */
    class MyClass { }
    
    inst = autoConstructor(MyClass);
    
    /* Should these work? */
    inst = autoConstructor<number>(Number);
    inst = autoConstructor(Number);
    inst = autoConstructor<string>(String);
    inst = autoConstructor(String);
    inst = autoConstructor(Date);
    
    /* Maybe: */
    
    // Ok
    autoConstructor<T extends MyClass>(ctor: constructorof T)......
    
    // Error: Types given to 'constructorof' must extend a class type 
    autoConstructor<T>(ctor: constructorof T)......
  5. RyanCavanaugh commented on Jul 17, 2015

    @RyanCavanaugh
    Member

    Note that new Number() does not produce a number (it's not primitive).

    You can already write this:

    interface constructorof<T> {
        new (): T;
    }
    
    function autoConstructor<T>(ctor: constructorof<T>): T {
      return new ctor();
    }
    
    var inst;
    
    /* Works */
    class MyClass { }
    
    let c = autoConstructor(MyClass); // c: MyClass
    
    /* Should these work? */
    inst = autoConstructor<Number>(Number);
    inst = autoConstructor(Number);
    inst = autoConstructor<String>(String);
    inst = autoConstructor(String);
    inst = autoConstructor(Date);
  6. AlicanC commented on Jul 17, 2015

    @AlicanC

    That doesn't provide the actual type for the constructor:

    class MyClass {
      static foo() {};
    }
    
    interface constructorof<T> {
        new (): T;
    }
    
    function autoConstructor<T extends MyClass>(ctor: constructorof<T>): T {
    
      // No foo here
      ctor.foo();
    
      return new ctor();
    }

    What is the reason for having typeof? Wouldn't constructorof cover all use cases of typeof and more? (And also without the created confusion with JavaScript's own typeof.)

  7. RyanCavanaugh commented on Jul 17, 2015

    @RyanCavanaugh
    Member

    There can be more than one constructor function for any given type.

    interface SomeObj { ... }
    declare var ctor1: {
      new(): SomeObj;
      foo;
    }
    declare var ctor2: {
      new(): SomeObj;
      bar;
    }
    
    function magic<T extends SomeObj>(ctor: constructorof<T>) {
     // can I use ctor.foo here? ctor.bar?
    }

    typeof exists so that, given a value of some type that is otherwise anonymous, you refer to that anonymous type without having to rewrite it yourself.

  8. AlicanC commented on Jul 17, 2015

    @AlicanC

    Can there be more than one constructor function for any given class type? If not then a constructorof keyword that only works with class types could solve the issue.

    class MyClass {
      static foo() {};
    }
    
    /*
      MyClass was declared with the `class` keyword.
      We can be sure that it has one constructor.
      We allow constructorof to be used with it.
    */ 
    var ctor: constructorof MyClass = MyClass;
  9. RyanCavanaugh commented on Jul 17, 2015

    @RyanCavanaugh
    Member

    Can there be more than one constructor function for any given class type?

    Yes.

    class Foo { }
    declare var factory: {
      new(): Foo;
    }
  10. AlicanC commented on Jul 17, 2015

    @AlicanC

    Yes, but typeof doesn't care about that:

    class Foo {
      static bar() {};
    }
    declare var factory: {
      new(): Foo;
      foo();
    }
    
    var ctor: typeof Foo = Foo;
    
    ctor.bar; // Here
    ctor.foo; // Not here

    The constructorof implementation can do the same.

  11. RyanCavanaugh commented on Jul 21, 2015

    @RyanCavanaugh
    Member

    I'm still not sure what you're proposing that is meaningfully different from typeof or new() => T.

  12. michaelmesser commented on Jun 9, 2016

    @michaelmesser

    Enum don't have constructors so new() => T does not work

  13. 7 remaining items

  14. RyanCavanaugh commented on Mar 24, 2017

    @RyanCavanaugh
    Member

    Ruan Mendes (@juanmendes) your return type should be new(payload: T) => TypedAction<T>, not TypedAction<T>. You've declared that you return the instance type but you've provided a constructor type

  15. juanmendes commented on Mar 24, 2017

    @juanmendes

    Thanks Ryan, it mostly works, the problem there is that the return of createTypedActionConstructor is not a true type, that is, I can't cast to it.

  16. markusmauch commented on Aug 11, 2017

    @markusmauch

    I don't understand why the typeof operator should not be applied on type arguments. The language specification states in section 4.18.6:

    The 'typeof' operator takes an operand of any type and produces a value of the String primitive type. In
    positions where a type is expected, 'typeof' can also be used in a type query (section 3.8.10) to produce
    the type of an expression
    .

    So why is something like the followin not allowed?

    getConstructor<T>( instance: T )
    {
        return instance.constructor as typeof T;
    }
    
  17. RyanCavanaugh commented on Aug 11, 2017

    @RyanCavanaugh
    Member

    typeof X produces the type of the value X. There's no value named T, so the "typeof the value T" question makes no sense. It's like asking how much blue weighs.

  18. markusmauch commented on Aug 12, 2017

    @markusmauch

    Ryan Cavanaugh (@RyanCavanaugh): But if so, why do the specs state that typeof can be used in a type query?

    class MyClass {}
    let x: typeof MyClass; // MyClass is a type here
    
  19. aluanhaddad commented on Aug 12, 2017

    @aluanhaddad
    Contributor

    Markus Mauch (@markusmauch)

    class MyClass {}
    let x: typeof MyClass; // MyClass is a value here

    x is a variable whose type is the type of the value named MyClass. A class definition introduces a name in the type space that refers to the type of instances of that class, and a name in the value space that refers to the class value (the thing that gets newed).

  20. markusmauch commented on Aug 13, 2017

    @markusmauch

    Aluan Haddad (@aluanhaddad)

    Well okay, MyClass is a value then. But how do I get the type of the constructor function of the type MyClass?

  21. aluanhaddad commented on Aug 13, 2017

    @aluanhaddad
    Contributor

    Markus Mauch (@markusmauch) by writing typeof MyClass in type position. Just as you had done.

  22. leoasis commented on Sep 21, 2017

    @leoasis

    Is there a reason typeof MyClass<SpecificType> is not supported when I have it defined as class MyClass<T> { ... }?

    I'd like to get the type of the class with a specific type, but without having to create a class that inherits from it:

    const MySpecificClass: typeof MyClass<SpecificType> = MyClass;
  23. aluanhaddad commented on Oct 4, 2017

    @aluanhaddad
    Contributor

    Leonardo Garcia Crespo (@leoasis) the type argument belongs to the instance side of the class. typeof operator obtains the type of the static side of the class.

  24. leoasis commented on Oct 4, 2017

    @leoasis

    Yeah, I know, that's what I wanted to show in the example. In my particular use case, I'm using React, and I have an InfiniteScrollList component that is parametrized on the type of the item it accepts. We did this to force the same type you provide to be the same type you would expect in any callback prop, instead of it being any and making the user specify the type:

    The problem there is that when I define the component with a generic type, I can no longer use it in JSX directly, since I cannot write

    return (
      <InfiniteScrollList<Item> {...props} />
    );

    So I tried doing something like this:

    const ItemInfiniteScrollList: typeof InfiniteScrollList<Item> = InfiniteScrollList;

    but that didn't work.

    Couldn't find another way to make that work without creating an empty class that inherits from the parametrized one. That works, but I would like to avoid creating a class (which will be created at runtime) just to enforce type safety.

  25. schmod commented on Nov 7, 2017

    @schmod

    The workaround mentioned by Salvatore Previti (@SalvatorePreviti) and the FAQ only works on classes that don't have any static methods, as the return type is only a constructor.

    I can't seem to come up with a decent workaround that would even allow for an identity function that accepts and returns a constructor.

    abstract class Animal<T> {
        static identity<T>(T: typeof Animal<T>) : typeof T; // this doesn't work
    }
    
    class Dog extends Animal<Dog> {
    }
    
    Animal.identity(Dog) // => Dog

    A more concrete (and realistic) use-case would be a static method that creates a new copy of the class/constructor (possibly with a different set of static properties, or extra static methods). This is a pattern often seen in ORMs, notably Sequelize's Model.scope() method.

    If anybody has ideas about similar ways to achieve the same result whilst working within the boundaries of TypeScript's existing type system, let's get them documented!

  26. aluanhaddad commented on Nov 7, 2017

    @aluanhaddad
    Contributor
    class Animal<T> {
        static identity<T extends new (...args: any[]) => Animal<any>>(C: T): T {
            return  C;
        }
    }
    
    class Dog extends Animal<Dog> {}
    
    Animal.identity(Dog); // => typeof Dog

    Or even

    class Animal<T> {
        static identity<T extends new (...args: any[]) => Animal<any>>(this: T) {
            return this;
        }
    
        static create<T extends Animal<any>>(this: new (...args: any[]) => T) {
            return new this();
        }
    }
    
    class Dog extends Animal<Dog> {}
    
    Dog.identity(); // => typeof Dog
    
    Dog.create(); // => Dog
  27. alfaproject commented on Mar 4, 2018

    @alfaproject

    I'm having the same problem, and basically ended up chain casting to any and typeof Model:

    // This class is in a 3rd party library:
    abstract class Model<T> {
      static count(): number;
    }
    
    interface Options<T extends Model<T>> {
      target: new () => T;
    }
    
    // `options` type is `Options<T>`
    const model = options.target as any as typeof Model;
    return model.count();
  28. locked and limited conversation to collaborators on Jul 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    By DesignDeprecated - use "Working as Intended" or "Design Limitation" instead

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions