带有 Typescript 和 ES6 代理的 MongoModel Class

MongoModel Class with Typescript and ES6 Proxy

我想在打字稿中实现 MongoModel class。 这类似于 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,但它不能处理所有 setters/getters。 我试过 Proxy 但不确定如何包装 Model class proxy.

嗯,我不知道 "MongoModel" 是什么,我也不知道 BasecollectionNamedocId 应该是什么。因此,如果以下内容不完全符合您的期望并且您无法适应它,您可能需要考虑编辑您的代码以构成 minimal reproducible example。但是让我们看看我们能在这里做什么。

首先,让我们将您的 Model 重命名为 InnerModel,这将用于实现您想要的类型,但不会向 as-is 用户公开:

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

请注意,我给了它一个构造函数来保存 T 类型的数据。现在的目标是制作一个像 Model<T> 这样的类型,它同时充当 TInnerModel<T>:

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

如果您有一个 Model<T> 实例,您可以直接访问 T 的所有属性,以及 InnerModel<T> 的所有属性。请注意,我完全忽略了如果 TInnerModel 有一些相同的 属性 名称会发生​​什么。那会很糟糕...如果 T{data: number, doSave: boolean} 你会很不开心。所以不要那样做。

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

实例的东西

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

首先,我们将添加一个助手 type guard function 来帮助区分 属性 名称是否是对象的已知键...接下来我们将使用它来帮助编译器了解 属性 名称是否是对象的已知键。 =89=] 键位于 InnerModel 本身或嵌套的 data 属性:

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

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

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() 处理程序 return anyvalue 是在 set() 处理程序中输入为 any。这主要关闭了类型检查,因此我们需要手动检查正确性。并且编译器看不到我们 returning Model<T> 而不是 InnerModel<T>,所以我们需要断言。

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

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

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

Link to code

编辑:如果你想让它成为 "flexible",你应该通过做 new Model<{[k: string]: any}>({}) 之类的事情来使 T "flexible",但是模型越灵活,少 strongly-typed 是的。是否要使用 indexable type 取决于您,但不会真正影响上面的实现(或者影响不大)