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

Suggestion: typeof of generic type #204

Closed
omidkrad opened this issue Jul 23, 2014 · 33 comments
Closed

Suggestion: typeof of generic type #204

omidkrad opened this issue Jul 23, 2014 · 33 comments
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead

Comments

@omidkrad
Copy link

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.

@RyanCavanaugh
Copy link
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

@omidkrad
Copy link
Author

The link was very informative. Thank you.

@AlicanC
Copy link

AlicanC commented Jul 17, 2015

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

@AlicanC
Copy link

AlicanC commented Jul 17, 2015

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

@RyanCavanaugh
Copy link
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);

@AlicanC
Copy link

AlicanC commented Jul 17, 2015

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

@RyanCavanaugh
Copy link
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.

@AlicanC
Copy link

AlicanC commented Jul 17, 2015

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;

@RyanCavanaugh
Copy link
Member

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

Yes.

class Foo { }
declare var factory: {
  new(): Foo;
}

@AlicanC
Copy link

AlicanC commented Jul 17, 2015

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.

@RyanCavanaugh
Copy link
Member

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

@michaelmesser
Copy link

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

@kitsonk
Copy link
Contributor

kitsonk commented Jun 10, 2016

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

And what problem is this presenting you with? Of course enum's don't have constructors. This was (originally) an issue about how to reference the constructor of a generic, which really turned into how to describe a generic that extends a constructor.

@michaelmesser
Copy link

I was making a method that took an enum and one of its values and typeof T said T is not found.

function x<T>(_enum: typeof T, value: T){}

@kitsonk
Copy link
Contributor

kitsonk commented Jun 11, 2016

This is a different problem... While the the intellisense lists an enumeration type as a typeof there is no way to narrow a generic to be a value of any enum to be able to try to extract the type it comes from. This is covered in #3440 as "accepting PRs". Once that is addressed, what you are talking about should work.

@josiahruddell
Copy link

josiahruddell commented Jun 21, 2016

I have the same problem, while it is understandable to have to pass the constructor as an argument to reference it later (although ironic since we already have the type parameter), the someFunc(constructor: new() => T) approach is unaware of static properties attached to the constructor arg. To get around the syntax error I had to use someFunc(constructor: new() => T | any). After which the compiler allows constructor.staticFoo.

Note, the someFunc(constructor: typeof ParentClass) can also get you half-way there as you can reference the static properties, however the compiler complains when you try to create an instance of the generic type from the constructor arg.

@SalvatorePreviti
Copy link

I found that a quite weird syntax may replace "typeof T" syntax.

/** Generic class type. */
type Constructor<T> = { new (...args: any[]): T } | ((...args: any[]) => T) | Function;

Possible usage:

/** Generic class type. */
type Constructor<T> = { new (...args: any[]): T } | ((...args: any[]) => T) | Function;


/** Dummy dependency injector */
export class Injector {

  public resolve<T>(x: Constructor<T>): T {
      // Injector implementation, here a dummy one.
      return undefined as any as T;
  }

}

/** A dummy base abstract class to resolve. */
export abstract class ClassToResolve {

  public n: number;

  protected abstract constructor(a: string, b: string) {
  }
}

let injector = new Injector();

const resolved = injector.resolve(ClassToResolve);

// Compiler says that the variable resolved is of type ClassToResolve, correct!
resolved.n = 1000;

// Compiler will fail here, xxx is not a member of ClassToResolve. Correct!
resolved.xxx = "xxx";

Well, it seems to "abuse" the TypeScript interpreter but it works, and makes sense.

Someone have an opinion over this and especially someone maybe knows if this would not work in the next version of TS?

@RyanCavanaugh
Copy link
Member

@SalvatorePreviti that's basically the same as the recommended solution in the FAQ, so I'd say it's well-supported 😉

@juanmendes
Copy link

I'm sorry to piggy back onto this, but I think my question is very much related to this question, but I couldn't quite figure out if it was so. http://stackoverflow.com/questions/43000982/how-to-create-a-factory-that-returns-anonymous-classes-that-implement-a-generic

I'm trying to do the following to remove boiler plate code of creating concrete classes for a payload type.

export interface TypedAction<T> extends Action {
    type: string;
    payload: T;
}
export function createTypedActionConstructor<T>(type: string): TypedAction<T> {
    return class TypedActionImpl implements TypedAction<T> {
        readonly type = type;
        constructor(public payload: T) {}
    }
}

But I get an error because what I'm returning is the class that implements TypedAction, not the instance. The error says:

TS2322 Type 'typeof TypedActionImpl' is not assignable to type 'TypedAction'. Property 'type' is missing.

Is it impossible to return a dynamic implementation of a generic class from TypeScript?

@RyanCavanaugh
Copy link
Member

@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

@juanmendes
Copy link

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.

@markusmauch
Copy link

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

@RyanCavanaugh
Copy link
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.

@markusmauch
Copy link

@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

@aluanhaddad
Copy link
Contributor

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

@markusmauch
Copy link

@aluanhaddad

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

@aluanhaddad
Copy link
Contributor

@markusmauch by writing typeof MyClass in type position. Just as you had done.

@leoasis
Copy link

leoasis commented Sep 21, 2017

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;

@aluanhaddad
Copy link
Contributor

@leoasis the type argument belongs to the instance side of the class. typeof operator obtains the type of the static side of the class.

@leoasis
Copy link

leoasis commented Oct 4, 2017

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.

@schmod
Copy link

schmod commented Nov 7, 2017

The workaround mentioned by @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!

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Nov 7, 2017

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

@alfaproject
Copy link

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();

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead
Projects
None yet
Development

No branches or pull requests