首页 > 解决方案 > 如何在 TypeScript 中使类实例可构造?

问题描述

我正在尝试将此包转换为 TypeScript,而无需进行任何重大更改。我在 TypeScript 中有以下代码。

// DocumentCarrier.ts
/* export */ class DocumentCarrier {
    internalObject: {};
    model: Model;
    save: (this: DocumentCarrier) => void;

    constructor(model: Model, object: {}) {
        this.internalObject = object;
        this.model = model;
    }
}
DocumentCarrier.prototype.save = function(this: DocumentCarrier): void {
    console.log(`Saved document ${JSON.stringify(this.model)} to ${this.model.myName}`);
};

// Model.ts
// import {DocumentCarrier} from "./DocumentCarrier.ts";
/* export */class Model {
    myName: string;
    Document: typeof DocumentCarrier;
    get: (id: number) => void;

    constructor(name: string) {
        this.myName = name;

        const self: Model = this;
        class Document extends DocumentCarrier {
            static Model: Model;

            constructor(object: {}) {
                super(self, object);
            }
        }
        Document.Model = self;

        Object.keys(Object.getPrototypeOf(this)).forEach((key) => {
            Document[key] = this[key].bind(this);
        });

        this.Document = Document;

        return this.Document as any;
    }
}
Model.prototype.get = function(id: number): void {
    console.log(`Retrieving item with id = ${id}`);
}

// Usage
// index.ts
// import {Model} from "./Model.ts";
const User = new Model("User");
const user = new User({"id": 5, "name": "Bob"});
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
// console.log(User.myName); // "User" // This option would be even better, but isn't supported in the existing code
User.get(5); // "Retrieving item with id = 5"

在这Usage部分(上面代码示例的最底部)中,我在 TypeScript 中遇到了多个错误。但是在 JavaScript 文件中运行该代码,效果很好。所以我知道它正在工作并且代码是准确的。

我认为我正在尝试做的最大问题是return this.Document as any. TypeScript 将其解释为转换this.Document为 Model 实例,而实际上并非如此。


我的问题是这个。在 TypeScript 中,我如何设置它可以运行new MyClassInstance()并让它返回不同类的实例?这具有来自MyClassInstance不同类的双向引用。简而言之,如何使以下代码正常工作?

任何解决方案都适用于该Usage部分,并且不对该部分进行任何修改,这一点很重要。除了User.Model.myNamevsUser.myName部分,它会被首选为User.myName,但在现有版本中的功能为User.Model.myName.


为了方便使用,我还创建了一个TypeScript Playground

标签: javascripttypescript

解决方案


我将把这个问题严格解释为“我怎样才能对现有代码进行类型化,以便编译器理解该Usage部分中的代码?” 也就是说,答案不应该触及发出的 JavaScript,而应该只改变类型定义、注释和断言。

除了:更一般的问题“我应该如何实现一个实例本身就是类构造函数的类”是一个我不会尝试解决的问题,因为根据我的研究,这里的最佳答案是“不要尝试这样做”,因为它与 JS 中的原型继承模型配合不佳。相反,我强烈倾向于让不可构造的类实例拥有一个属性,该属性是新类的构造函数。像这样的游乐场代码。我预计,从长远来看,你会更快乐。

回到类型:这里的主要问题是 TypeScript 无法指定类构造函数返回的类型不是被定义的类。这是有意的(参见microsoft/TypeScript#11588或缺少的功能(参见microsoft/TypeScript#27594),但无论如何它不是语言的一部分。

我们在这里可以做的是使用声明合并。当您编写时,class Model {}您同时引入了一个名为的类构造函数对象Model和一个名为 的接口类型Model。可以合并该接口,添加编译器尚不知道的方法和属性。在您的情况下,您可以这样做:

interface Model {
    new(object: {}): DocumentCarrier;
    Model: Model;
}

这让编译器知道Model实例,除了在类中声明的属性/方法外,还有一个Model类型为 的属性Model,重要的是,还有一个构造函数签名。这足以让以下代码无错误地编译:

const User = new Model("User");
const user = new User({ "id": 5, "name": "Bob" });
user.save(); // "Saved document {"id": 5, "name": "Bob"} to User"
console.log(User.Model.myName); // "User"
User.get(5); // "Retrieving item with id = 5"

编译器确实认为User.myName存在,它在运行时不存在,但这已经是现有代码的问题,所以我在这里不涉及。可以进一步更改类型,以便编译器知道User.Model.myName存在和User.myName不存在,但这变得非常复杂,因为它需要您将Model的接口拆分为您仔细分配给正确值的多种类型。所以现在我忽略它。

我在这里要做的唯一其他更改是为 的实现提供不同的类型Model,如下所示:

class Model {
    myName: string;
    Document: Model;
    get!: (id: number) => void;

    constructor(name: string) {
        this.myName = name;

        const self: Model = this;
        class Document extends DocumentCarrier {
            static Model: Model;

            constructor(object: {}) {
                super(self, object);
            }
        }
        Document.Model = self;

        (Object.keys(Object.getPrototypeOf(this)) as
            Array<keyof typeof DocumentCarrier>).forEach((key) => {
                Document[key] = this[key].bind(this);
            });

        this.Document = Document as Model;

        return this.Document;
    }
}

编译器在上面唯一无法验证的是Document类是有效的Model,所以我们使用断言Document as Model。除此之外,我只是提出了一些断言(肯定get赋值Object.keys()并将返回构造函数的键数组DocumentCarrier),这样您就不需要关闭--strict编译器标志。


好的,希望有帮助。祝你好运!

Playground 代码链接


推荐阅读