我有这个示例 TypeScript 代码,应该将一个简单的 JSON 反序列化为类
Person
的实例,然后对其调用 foo
方法,但它不起作用:
class Person {
name!: string;
age!: number;
foo() {
console.log("Hey!");
}
}
fetch("/api/data")
.then(response => {
return response.json() as Promise<Person>;
}).then((data) => {
console.log(data);
data.foo();
});
控制台的输出显示该对象处于正确的形状,但它没有被识别为
Person
:
Object { name: "Peter", age: 44 }
age: 44
name: "Peter"
因此,当它尝试调用
foo
方法时,它会失败:
未捕获(承诺中)类型错误:data.foo 不是函数
http://127.0.0.1:8000/app.js:14
承诺回调* http://127.0.0.1:8000/app.js:12
我该如何修复它?我应该使用
Object.assign
还是有其他更好/native 解决方案?
let x = (<any>Object).assign(Object.create(Person.prototype), data);
x.foo();
请记住,TypeScript 只是一种使用类型保护注释 JavaScript 代码的方法。它不做任何额外的事情。例如,说
response.json()
返回的对象应被视为 Promise<Person>
并不意味着它将调用 Person
类的构造函数。相反,您只会得到一个具有名称和年龄的普通旧 JavaScript 对象。
在我看来,您需要为
Person
类创建一个构造函数,该构造函数可以基于与其接口匹配的对象创建 Person 的新实例。也许是这样的?
interface PersonLike {
name: string;
age: string;
}
class Person implements PersonLike {
constructor(data: PersonLike) {
this.name = data.name;
this.age = data.age;
}
name: string;
age: string;
foo() {
console.log("Hey!");
}
}
fetch("/api/data")
.then(response => {
return response.json() as Promise<PersonLike>;
}).then((data) => {
const person = new Person(data);
person.foo();
});
我还建议使用类型保护而不是
as
关键字,以防您从更改中获取数据的 API。也许是这样的:
function isPersonLike(data: any): data is PersonLike {
return typeof data?.name === 'string' && data?.age === 'string';
}
fetch("/api/data")
.then(response => {
return response.json();
}).then((data: unknown) => {
if (isPersonLike(data)) {
const person = new Person(data);
person.foo();
}
});
...应该将一个简单的 JSON 反序列化为类 Person 的实例,然后...
不幸的是,TypeScript 中的泛型类型只能充当某种模型设计助手。它永远不会被编译成 JavaScript 文件。以您的“获取”代码为例:
fetch("/api/data")
.then(response => {
return response.json() as Promise<Person>;
}).then((data) => {
console.log(data);
data.foo();
});
将上面的 TypeScript 文件编译成 JavaScript 后,我们可以发现代码
as Promise<Person>
被完全删除了:
fetch("/api/data")
.then(function (response) {
return response.json();
}).then(function (data) {
console.log(data);
data.foo();
});
要实现“类型安全的反序列化”,需要在序列化过程中保存类/原型信息。否则,这些类/原型信息将会丢失。
...或者还有另一个更好/本机的解决方案? ...顺便说一句,如果类字段具有自定义类型,因此它是另一个类的实例怎么办?
不,没有原生解决方案,但您可以使用一些库实现“类型安全”序列化/反序列化。
我制作了一个名为 esserializer 的 npm 模块来自动解决此问题:在序列化期间以纯 JSON 格式保存 JavaScript 类实例值及其类名信息。随后,在反序列化阶段(可能在另一个进程或另一台机器上),esserializer 可以使用相同的类定义递归地反序列化对象实例,并保留所有类/属性/方法信息。对于您的“获取”代码案例,它看起来像:
// Node.js server side, serialization happens here.
const ESSerializer = require('esserializer');
router.get('/api/data', (req, res) => {
// ...
res.json(ESSerializer.serialize(anInstanceOfPerson));
});
// Client side, deserialization happens here.
const ESSerializer = require('esserializer');
fetch("/api/data")
.then(response => {
return response.text() as Promise<string>;
}).then((data) => {
const person = ESSerializer.deserialize(data, [Person, CustomType1, CustomType2]);
console.log(person);
person.foo();
});
TypeScript 中最流行的类型安全反序列化库是 io-ts,在撰写本文时,每周下载量超过一百万次。我还可以推荐一个辅助库 io-ts-promise。
我们已在中等规模生产后端应用程序中使用它们多年。没有前端使用经验。
令人惊讶的是,类型安全反序列化并不是 TypeScript 语言的一部分。
请记住,函数无法序列化为 JSON、XML 或任何其他流行格式。您不能只是将一些 JSON 对象转换为具有函数的类,并期望它现在具有该类的函数。其他答案已经指出了如何处理这个问题。