返回类构造函数的函数的类型声明

3

我正在为一个具有全局结构的JavaScript游戏引擎编写TypeScript声明文件,但我不太清楚如何处理返回类构造函数的函数。以下是代码的简化版本:

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

  return script;
}

用户可以按照以下方式扩展script类构造函数:

var MyScript = createScript('myScript');

MyScript.someVar = 6;

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

如何在声明文件中处理createScript函数和script类构造函数?

更新 我已经更新了我的代码示例,以显示用户还应该扩展“类”的静态部分。到目前为止给出的答案虽然很棒,但似乎不允许这样做。


2
附注:函数的 name 属性是只读的。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/name - Robert Penner
@RobertPenner 你说得完全正确。我错了,我会编辑代码的。 - snowfrogdev
如果您绝对需要函数名称是动态的,您可以使用 evalnew Function。https://dev59.com/lm025IYBdhLWcg3wnXSf - Robert Penner
2个回答

3
你可以将createScript的返回类型声明为某种通用脚本构造函数。
interface AbstractScript { 
    app;
    entity;
}

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

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

然后

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

注意:为了方便起见,创建的类的构造函数和接口都被命名为Foo
一开始以为TS可能会有问题,但实际上它能够很好地区分它们。

1
类型定义很好。有几个拼写错误:ScriptConsturctor: String应该小写。 - Robert Penner
1
抱歉措辞不够明确,但我认为ScriptConsturctor中的拼写错误("u"和"r"交换)是显而易见的。 - Robert Penner

2
在TypeScript中,表示类构造函数的一种可能方式是使用带有所谓“构造函数签名”的接口。因为createScript应该返回用户创建(或更准确地说,用户修改)类的实例,所以它必须是泛型的。用户将不得不提供一个描述扩展类的接口作为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>;

使用createScript时,用户必须描述扩展类并将其实现分别分配给原型。
interface MyScript extends Script {
    update(dt: {}): void;
}
var MyScript = createScript<MyScript>('myScript');

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

更新:
如果您希望用户也能够扩展构造函数类型(以自定义类的“静态”部分),那么您可以通过一些额外的工作来实现。这涉及添加另一个泛型参数以用于自定义构造函数类型。用户还必须提供描述该类型的接口 - 在此示例中为MyScriptClass
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;

请注意,在这个解决方案中,编译器并没有真正检查提供的实现是否符合声明的接口 - 你可以将任何东西分配给prototype.update,编译器不会抱怨。此外,在分配prototype.updatesomeVar之前,当您尝试使用它们时,您会得到未定义的结果,并且编译器也不会捕获这一点。
另一个更新: prototype.udpate的赋值不进行类型检查的原因是,某种方式推断出MyScript的静态部分是一个Function,而Function.prototype内置库中被声明为any,这会抑制类型检查。有一个简单的解决方法:只需声明自己更具体的prototype即可。
export interface ScriptConstructor<S extends Script> {
    _name: string;
    new(args: ScriptConstructorArgs): S;
    prototype: S;
}

然后它开始捕捉这样的错误:
MyScript.prototype.udpate = function(dt) {
  // Game programming stuff
}
// Property 'udpate' does not exist on type 'MyScript'. Did you mean 'update'?

此外,dt参数类型也是从MyScript接口推断出来的。
实际上,我没有太多像这样做事情的经验,所以我不知道声明自己的原型是否会在将来与其他代码造成问题。
另外,如果完全省略对prototype.update进行赋值,它也不会抱怨——因为它在createScript类型中被声明,它依然相信update存在。这是从JavaScript实现中“组装”一个类与一些外部类型声明的代价——“正常”的、全TypeScript创建类的方式只需使用类似于class MyScript extends ScriptBase implements SomeInterface的语法,这样就可以保证类型检查。

谢谢你的好答案。我很好奇,为什么编译器不会检查实现是否符合声明的接口? - snowfrogdev
这个效果很好,谢谢。我在想,我们有两个接口,一个是实例端口,一个是类的静态(构造函数)端口。是否可能编写一个适当的TypeScript类来实现这两个实例? - snowfrogdev
1
它看起来像这样:class S { a(): void { } static b: number; } 你可以将静态部分称为 typeof S。然而,据我所知,没有办法声明静态部分必须符合某个接口。 - artem

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接