在 TypeScript 中构建可重新序列化的包装器 class

Building a re-serializable wrapper class in TypeScript

我正在开发一个用于复杂 API 模型的实用程序库。我们从线路接收 JSON- 解析的对象,并对它们的结构有某种保证,如下所示:

// Known structure of the message:
interface InputBox {
  top: number;
  height: number;
  left: number;
  width: number;
}
interface InputObj {
  box: InputBox
}

// User code (outside our scope) does something like:
const inputObj: InputObj = JSON.parse(
  '{ "box": { "top": 0, "height": 10, "left": 1, "width": 2, "color": "red" } }'
);

我的目标是创建对象的一些视图:

例如,用户代码可能如下所示:

// In-place or new obj constructor would both be fine:
const easyObj = myCoolLibrary(inputObj);

easyObj.box.top = 5;
console.log(easyObj.box.getBottom());  // '15'

JSON.stringify(easyObj);
// -> { "box": { "top": 5, "height": 10, "left": 1, "width": 2, "color": "red" } }
// (Note box.color still present, although missing from the input interface)

阅读了一些选项后,似乎是:

问题是,即使这些方法可行,我也看不出如何以让 TypeScript 满意的方式实现它们?

例如:

class MyCoolBox implements InputBox {
  constructor(box: InputBox) {
    Object.assign(this, box);
  }

  getBottom() {
    return this.top + this.height;
  }
}
// > Class 'MyCoolBox' incorrectly implements interface 'InputBox'.
// (and doesn't recognise presence of .top, .height)

Object.setPrototypeOf(inputObj.box, MyCoolBox);
inputObj.box.getBottom();
// > Property 'getBottom' does not exist on type 'InputBox'
// Doesn't recognise the change of interface.

我是否缺少一些 TypeScript 可以理解的明智方法?用一些方法装饰一个 JSON-parsed 对象(已知接口)似乎是一个合理的要求!

首先,您似乎对 implements 的工作原理有误解。这些属性被识别,只是您告诉编译器您 class 实现了 这些属性,但从未 实际上 实现了它们。是的,您在构造函数中执行了 Object.assign,它在 运行时 达到了预期的结果,但编译器不知道。

由于您显然不想在 class 上拼出 InputBox 的所有可能的 属性,解决方案是改为使用 class es 绑定了同名的接口。因此,您可以使 MyCoolBox 接口成为您希望扩展的接口的 子类型 (即 InputBox):

interface MyCoolBox extends InputBox { }

class MyCoolBox {
  constructor(box: InputBox) {
    Object.assign(this, box);
  }

  getBottom() {
    return this.top + this.height;
  }
}

其次,您希望 setPrototypeOf 表现得像某种类型保护,而它被简单地定义为:

ObjectConstructor.setPrototypeOf(o: any, proto: object | null): any

意味着通过更改原型,您不会改变编译器对 box 对象形状的了解:对它来说,它仍然是一个 InputBox。还有一个问题是JavaScript中的classes 主要是函数+基于原型继承的语法糖

当您尝试调用 getBottom 方法时,将 box 原型设置为 MyCoolBox 将在运行时失败,因为它 不是静态的 ,并且您仅以这种方式设置 class 的静态端。您真正想要的是设置 MyCoolBoxprototype - 这将设置实例属性:

const myCoolLibrary = <T extends InputObj>(input: T) => {
  Object.setPrototypeOf(input.box, MyCoolBox.prototype); //note the prototype
  return input as T & { box: MyCoolBox };
};

const enchanced = myCoolLibrary(inputObj);
const bottom = enchanced.box.getBottom(); //OK
console.log(bottom); //10

最后,如上例,需要告诉编译器输出类型为增强型。您可以像上面那样使用简单的 type assertion (as T & { box: MyCoolBox }) 或让编译器为您推断增强类型:

{
  const myCoolLibrary = (input: InputObj) => {
    return {
      ...input,
      box: new MyCoolBox( input.box )
    }
  };

  const enchanced = myCoolLibrary(inputObj);
  const bottom = enchanced.box.getBottom(); //OK
  console.log(bottom); //10
}

Playground