在 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" } }'
);
我的目标是创建对象的一些视图:
- 添加了一些实用函数和属性
- 字符串化为 same/compatible JSON 作为输入
- 当被视为可变时表现一致
- 尽可能保留任何意外的属性
例如,用户代码可能如下所示:
// 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)
阅读了一些选项后,似乎是:
- 可能有一个很好的就地解决方案,使用 Object.setPrototypeOf 直接覆盖输入中的原型,但这是最近的事情,不确定是否适用于旧浏览器的 polyfilling。
- 创建新对象(使用 classes/prototypes)并使用 Object.assign 克隆属性可以工作,因为已知输入对象是 simple/boring JSON-able
Object
s... 仍然需要在 IE 上使用 polyfill,但也许更标准一些?
问题是,即使这些方法可行,我也看不出如何以让 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中的class
es 主要是函数+基于原型继承的语法糖
当您尝试调用 getBottom
方法时,将 box
原型设置为 MyCoolBox
将在运行时失败,因为它 不是静态的 ,并且您仅以这种方式设置 class 的静态端。您真正想要的是设置 MyCoolBox
的 prototype - 这将设置实例属性:
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
}
我正在开发一个用于复杂 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" } }'
);
我的目标是创建对象的一些视图:
- 添加了一些实用函数和属性
- 字符串化为 same/compatible JSON 作为输入
- 当被视为可变时表现一致
- 尽可能保留任何意外的属性
例如,用户代码可能如下所示:
// 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)
阅读了一些选项后,似乎是:
- 可能有一个很好的就地解决方案,使用 Object.setPrototypeOf 直接覆盖输入中的原型,但这是最近的事情,不确定是否适用于旧浏览器的 polyfilling。
- 创建新对象(使用 classes/prototypes)并使用 Object.assign 克隆属性可以工作,因为已知输入对象是 simple/boring JSON-able
Object
s... 仍然需要在 IE 上使用 polyfill,但也许更标准一些?
问题是,即使这些方法可行,我也看不出如何以让 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中的class
es 主要是函数+基于原型继承的语法糖
当您尝试调用 getBottom
方法时,将 box
原型设置为 MyCoolBox
将在运行时失败,因为它 不是静态的 ,并且您仅以这种方式设置 class 的静态端。您真正想要的是设置 MyCoolBox
的 prototype - 这将设置实例属性:
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
}