首页 > 解决方案 > 带有 Typescript 和 ES6 代理的 MongoModel 类

问题描述

我想在打字稿中实现 MongoModel 类。这类似于 php 或 rails 中的 ActiveRecord。

class Model {
    data: any; 
}
let model = new Model();

我想要完成的是

而不是model.data.name = 'John',我更愿意model.name = 'John'。更复杂的是该字段应该是灵活的(就像 mongo 一样)。 model['anything'] = 'can be assigned'最终将在数据属性中分配。将与 相同model.data['anything'] = 'can be anything'

我试过defineProperty,但它不能处理所有的setter/getter。我尝试过 Proxy,但不确定如何包装 Model 类代理。

标签: typescriptmodeles6-class

解决方案


嗯,我不知道“MongoModel”是什么,也不知道Base应该collectionNamedocId什么。因此,如果以下内容与您的期望不完全匹配并且您无法对其进行调整,您可能需要考虑编辑您的代码以构成一个最小的可重现示例。但是让我们看看我们可以在这里做什么。

首先,让我们重命名您的Modelto InnerModelwhich 将用于实现您所需的类型,但不会按原样暴露给用户:

class InnerModel<T> {
  constructor(public data: T) {}
  doSave(): boolean {
    console.log("saving");
    return true; // implement me
  }
}

请注意,我给了它一个构造函数来保存 type 的数据T。现在的目标是创建一个Model<T>既充当 aT又充当 a 的类型InnerModel<T>

type Model<T> = T & InnerModel<T>;

如果您有Model<T>实例,则可以直接访问 的所有属性T,以及 的所有属性InnerModel<T>。请注意,如果T某些属性名称与InnerModel. 那会很糟糕......如果T{data: number, doSave: boolean}会度过一段糟糕的时光。所以不要那样做。

无论如何,这里的目标是制作一些实际构造Model<T>.

请注意,编译器无法真正验证您从这里开始执行的操作是否是类型安全的,因此您需要使用类型断言或等效方法来防止编译器发出错误。这意味着你必须小心,你只断言你可以自己验证的东西。

首先,我们将添加一个辅助类型保护函数来帮助区分属性名称是否是对象的已知键......接下来我们将使用它来帮助编译器了解属性键是在InnerModel自身上还是在嵌套data属性上:

function hasKey<T extends object>(obj: T, k: keyof any): k is keyof T {
  return k in obj;
}

这是实现的主要部分...使用代理将属性获取/设置路由到模型或数据,具体取决于找到密钥的位置

function makeModel<T>(data: T): Model<T> {
  return new Proxy(new InnerModel(data), {
    get(model: InnerModel<T>, prop: keyof Model<T>) {
      return hasKey(model, prop) ? model[prop] : model.data[prop];
    },
    set(model: InnerModel<T>, prop: keyof Model<T>, value: any) {
      return hasKey(model, prop)
        ? (model[prop] = value)
        : (model.data[prop] = value);
    }
  }) as Model<T>;
}

有几个地方这不是类型安全的......get()set()处理程序返回any,并且在处理程序value中输入。这主要是关闭类型检查,所以我们需要手动检查正确性。并且编译器看不到我们返回的是 a而不是 an ,所以我们需要断言。anyset()Model<T>InnerModel<T>

最后,我们将采用该makeModel()函数并将其视为构造函数。在 JavaScript 中,您可以使用任何函数作为构造函数,如果该函数返回一个值,则构造的对象将是该返回值。编译器真的不喜欢我们这样做,所以需要一个双重断言:

const Model = makeModel as unknown as new <T>(data: T) => Model<T>;

但是现在我们有了一些有用的东西:

const n = new Model({
  a: "hey",
  b: 1,
  c: true,
  d() {
    console.log("D");
  }
});

console.log(n.a); // hey
n.a = "you";
console.log(n.a); // you
console.log(n.data.a); // you
n.d(); // D
n.doSave(); // saving

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

链接到代码

编辑:如果你想让它“灵活”,你应该T通过做类似的事情来“灵活” new Model<{[k: string]: any}>({}),但是模型越灵活,它的强类型就越少。是否要使用可索引类型取决于您,但不会真正影响上面的实现(或者反正影响不大)


推荐阅读