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.