Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript convert type `typeof Foo` to `Foo`

I'm working on a generic Typescript interface where a factory class is instantiated with a particular class and has methods responsible for creating various instances of that class. My ideal type interface, which I cannot seem to achieve, is as follows:

class BaseModel { /* impl */ }
class Foo extends BaseModel { /* impl */ }
class Factory<T extends BaseModel> { /* impl */ }

let factory : Factory<Foo> = new Factory(Foo)
let fooInstance = factory.build() // returns type: `Foo`

However I cannot figure out how to get a declaration of Factory<T> to achieve this without either compromising the return type or maintaining a parallel constructor type definition.

If I rely on nothing but the types given by the classes I've declared, the return type of the build function is always BaseClass, plus I have to make the generic Factory<typeof Foo> instead of Factory<Foo>:

class BaseModel {
  static classMethod() : string {
    return 'i am class method'
  }

  hello() {
    return 'hello world'
  }
}
class Foo extends BaseModel {}

class Factory<T extends typeof BaseModel> {
  private _modelClass : T

  constructor(modelClass : T) {
    this._modelClass = modelClass
  }

  build() {
    return new this._modelClass()
  }

  echoClassMethod() {
    console.log(this._modelClass.classMethod())
  }
}

let factory : Factory<typeof Foo> = new Factory(Foo)
let fooInstance = factory.build() // Returns type `BaseClass` instead of `Foo`

But if I want the factory type interface to work correctly, I have to maintain a parallel type definition that mirrors typeof BaseModel, but as a constructor function instead:

class BaseModel {
  static classMethod() : string {
    return 'i am class method'
  }

  hello() {
    return 'hello world'
  }
}
class Foo extends BaseModel {}

// Have to maintain a separate type that 
// mirrors the class interface of my target class
type BaseModelConstructor<T extends BaseModel> = {
  new(...args: any[]) : T 
  classMethod() : string
}

class Factory<T extends BaseModel> {
  private _modelClass : BaseModelConstructor<T>

  constructor(modelClass : BaseModelConstructor<T>) {
    this._modelClass = modelClass
  }

  build() {
    return new this._modelClass()
  }

  echoClassMethod() {
    console.log(this._modelClass.classMethod())
  }
}

let factory : Factory<Foo> = new Factory(Foo)
let fooInstance = factory.build() // Correctly returns type `Foo`

There has to be a better way to convert a typeof X into an X or vice versa?

like image 298
Wade Tandy Avatar asked Dec 29 '17 20:12

Wade Tandy


1 Answers

Assuming X is the both the name of a constructor value and the name of the instance type (which is what happens when you declare class X {...}):


UPDATE:

As of TypeScript 2.8, there is a predefined type function called InstanceType<> which takes the type of a class constructor and evaluates to the type of its instance, using conditional type inference instead of a lookup. So you can now get from typeof X to X by using InstanceType<typeof X>. But (typeof X)['prototype'] below still works.


To get from typeof X to X, you can usually lookup the prototype property of the typeof X constructor type. For example, in the following I've annotated the return type of build() to be T['prototype']:

class Factory<T extends typeof BaseModel> {
  private _modelClass : T

  constructor(modelClass : T) {
    this._modelClass = modelClass
  }

  // note the declared return type
  build(): T['prototype'] {
    return new this._modelClass()
  }

  echoClassMethod() {
    console.log(this._modelClass.classMethod())
  }
}

And then the following works:

let factory = new Factory(Foo)
let fooInstance = factory.build() // Foo, as desired.

Does that help? Good luck!

like image 106
jcalz Avatar answered Oct 03 '22 18:10

jcalz