简体   繁体   中英

Type declaration for function that returns a class constructor

I'm writing a TypeScript declaration file for a JavaScript game engine with a global structure and I don't quite know how to deal with this function that returns a class constructor. Here is a simplified version of the code:

var createScript = function(name) {
  var script = function(args) {
    this.app = args.app;
    this.entity = args.entity;
  }
  script._name = name

  return script;
}

Users are meant to extend the script class constructor with their own code like this:

var MyScript = createScript('myScript');

MyScript.someVar = 6;

MyScript.prototype.update = function(dt) {
  // Game programming stuff
}

What is the proper way to deal with the createScript function and script class constructor in my declaration file?

UPDATE I have updated my code example to show that users are also expected to extend the static side of the "class". The answers given so far, though awesome, don't seem to allow for that.

you can declare the returntype of createScript as some kind of generic script constructor

interface AbstractScript { 
    app;
    entity;
}

interface ScriptConstructor<T extends AbstractScript> { 
    new(args: AbstractScript): T;
    readonly prototype: T;
}

declare function createScript(name: string): ScriptConstructor<any>;

and then

interface Foo extends AbstractScript { 
    foo(): void;
}

let Foo:ScriptConstructor<Foo> = createScript("Foo");
Foo.prototype.foo = function () { }

let d = new Foo({app: "foo", entity: "bar"});
d.foo();
d.entity
d.app

Note: named the constructor and the interface of the created class both Foo for conveniance reasons.
Thaught first, TS might have an issue with that, but no, it differentiates nicely.

In TypeScript, one possible way to represent a class constructor is by using an interface with so-called constructor signature . Because createScript is supposed to return an instance of user-created (or, more precisely, user-modified) class, it has to be generic. The user will have to provide an interface that describes extended class as generic paratemeter for createScript :

export interface ScriptConstructorArgs {
    app: {};     // dummy empty type declarations here
    entity: {}; 
}
export interface Script {  // decsribes base class
    app: {};
    entity: {};
}

export interface ScriptConstructor<S extends Script> {
    _name: string;
    new(args: ScriptConstructorArgs): S;
}

// type declaration for createScript
declare var createScript: <S extends Script>(name: string) => ScriptConstructor<S>;

When using createScript , users must describe extended class and separately provide its implementation by assigning to the prototype

interface MyScript extends Script {
    update(dt: {}): void;
}
var MyScript = createScript<MyScript>('myScript');

MyScript.prototype.update = function(dt) {
  // Game programming stuff
}

UPDATE

If you want users to be able to extend constructor type too (to customize "static" side of the class), you can do that with some extra work. It involves adding another generic parameter for customized constructor type. Users also have to provide an interface describing that type - MyScriptClass in this example:

export interface ScriptConstructorArgs {
    app: {};     // dummy empty type declarations here
    entity: {}; 
}
export interface Script {  // decsribes base class
    app: {};
    entity: {};
}

export interface ScriptConstructor<S extends Script> {
    _name: string;
    new(args: ScriptConstructorArgs): S;
}

// type declaration for createScript
declare var createScript: <Instance extends Script, 
                           Class extends ScriptConstructor<Instance>
                        >(name: string) => Class;

interface MyScript extends Script {
    update(dt: {}): void;
}
interface MyScriptClass extends ScriptConstructor<MyScript> {
    someVar: number;
}
var MyScript = createScript<MyScript, MyScriptClass>('myScript');

MyScript.prototype.update = function(dt) {
  // Game programming stuff
}

MyScript.someVar = 6;

Just be aware that in this solution the compiler does not really check that provided implementation conforms to the declared interface - you can assign anything to prototype.update and compiler will not complain. Also, until prototype.update and someVar are assigned, you get undefined when you try to use them, and compiler will not catch that too.

ANOTHER UPDATE

The reason why the assignment to prototype.udpate is not typechecked is that somehow the static part of MyScript is inferred to be a Function , and Function.prototype is declared in the built-in library as any , which suppresses type checking. There is an easy fix: just declare your own, more specific prototype :

export interface ScriptConstructor<S extends Script> {
    _name: string;
    new(args: ScriptConstructorArgs): S;
    prototype: S;
}

then it starts catching errors like this:

MyScript.prototype.udpate = function(dt) {
  // Game programming stuff
}
// Property 'udpate' does not exist on type 'MyScript'. Did you mean 'update'?

Also, dt parameter type is also inferred from the MyScript interface.

Actually I don't have much experience doing stuff like this, so I don't know if declaring your own prototype could cause some problems with other code in the future.

Also, it will not complain if the assignment to prototype.update is missing altogether - it will still believe update is there because it was declared in createScript type. That's the price to pay for "assembling" a class from javascript implementation together with some external type declarations - the "normal", all-typescript way to create a class is just use something like class MyScript extends ScriptBase implements SomeInterface , then it's guaranteed to be typechecked.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM