typescript - 如何序列化 Typescript 类型信息?
问题描述
我有一个ContentGateway
如下所示的界面:
interface ContentGateway {
register: (eventType: EventType) => E.Either<Error, void>;
send: <T> (event: T) => E.Either<Error, void>;
}
这样做的方式是,首先您必须注册一个事件,然后您才能开始发送它。
我对类型信息进行了如下编码:
enum FieldType {
STRING,
BOOLEAN,
NUMBER
}
type Field ={
namespace: string;
name: string;
type: FieldType;
}
type EventType = {
version: string;
name: string;
fields: Field[];
}
这并不理想,因为理想情况下它的样子是这样的:
interface ContentGateway {
register: <T> (eventType: T) => E.Either<Error, void>;
send: <T> (event: T) => E.Either<Error, void>;
}
问题是调用者和被调用者之间存在进程障碍(通常是 REST 接口),所以我需要序列化类型信息。
如果我事先知道双方都会使用 Typescript,有没有办法序列化 Typescript 类型信息?
我知道io-ts:
import * as t from 'io-ts'
const User = t.type({
userId: t.number,
name: t.string
})
但它只支持 TypeOf 操作:
type User = t.TypeOf<typeof User>
// same as
type User = {
userId: number
name: string
}
不是相反。
编辑:我知道 Typescript 类型在运行时不存在,但io-ts
Type
可以序列化对象。例如,当我通过网络发送时,上面的示例将如下所示:
{
"name":"User",
"props":{
"id":{
"name":"number",
"_tag":"NumberType"
},
"name":{
"name":"string",
"_tag":"StringType"
}
},
"_tag":"InterfaceType"
}
所以问题是如何Type
从这个json中恢复对象
解决方案
t.Type
从对象序列化中恢复实例
我过去用于类似事情的方法是反映标签名称以遍历任意类型信息。
import { absurd, pipe } from "fp-ts/lib/function";
import * as E from "fp-ts/lib/Either";
import * as R from "fp-ts/lib/ReadonlyRecord";
import * as t from "io-ts";
const InterfaceMetaCodec = t.type({
_tag: t.literal("InterfaceType"),
props: t.UnknownRecord
});
const ArrayMetaCodec = t.type({
_tag: t.literal("ArrayType"),
type: t.unknown
});
const UnionMetaCodec = t.type({
_tag: t.literal("UnionType"),
types: t.UnknownArray
});
const IntersectionMetaCodec = t.type({
_tag: t.literal("IntersectionType"),
types: t.UnknownArray
});
const makeSimpleMetaCodec = <T extends string>(tag: T) =>
t.type({
_tag: t.literal(tag)
});
const NullMetaCodec = makeSimpleMetaCodec("NullType");
const UndefinedMetaCodec = makeSimpleMetaCodec("UndefinedType");
const VoidMetaCodec = makeSimpleMetaCodec("VoidType");
const BooleanMetaCodec = makeSimpleMetaCodec("BooleanType");
const NumberMetaCodec = makeSimpleMetaCodec("NumberType");
const StringMetaCodec = makeSimpleMetaCodec("StringType");
const AnyMetaCodec = makeSimpleMetaCodec("AnyType");
const UnknownMetaCodec = makeSimpleMetaCodec("UnknownType");
const LiteralMetaCodec = t.type({
_tag: t.literal("LiteralType"),
value: t.union([t.number, t.string, t.boolean])
});
const MetaCodec = t.union([
NullMetaCodec,
UndefinedMetaCodec,
VoidMetaCodec,
BooleanMetaCodec,
NumberMetaCodec,
StringMetaCodec,
IntersectionMetaCodec,
UnionMetaCodec,
InterfaceMetaCodec,
LiteralMetaCodec,
AnyMetaCodec,
UnknownMetaCodec,
ArrayMetaCodec
]);
const sequenceRecord = R.sequence(E.either);
const recursivelyDecode = (type: unknown): E.Either<t.Errors, t.Mixed> =>
pipe(
type,
MetaCodec.decode,
E.chain((cursor) => {
switch (cursor._tag) {
case "InterfaceType": {
return pipe(
Object.fromEntries(
Object.entries(cursor.props).map(
([key, p]) => [key, recursivelyDecode(p)] as const
)
),
sequenceRecord,
E.map((props) => t.type(props))
);
}
case "UnionType": {
return pipe(
cursor.types.map(recursivelyDecode),
E.sequenceArray,
E.map((ts) => t.union(ts as [t.Mixed, t.Mixed, ...t.Mixed[]]))
);
}
case "IntersectionType": {
return pipe(
cursor.types.map(recursivelyDecode),
E.sequenceArray,
E.map((ts) => t.intersection(ts as [t.Mixed, t.Mixed]))
);
}
case "ArrayType": {
return pipe(
recursivelyDecode(cursor.type),
E.map((aT) => t.array(aT))
);
}
case "LiteralType": {
return E.right(t.literal(cursor.value));
}
case "StringType": {
return E.right(t.string);
}
case "BooleanType": {
return E.right(t.boolean);
}
case "NumberType": {
return E.right(t.number);
}
case "NullType": {
return E.right(t.null);
}
case "UndefinedType": {
return E.right(t.undefined);
}
case "VoidType": {
return E.right(t.void);
}
case "AnyType": {
return E.right(t.any);
}
case "UnknownType": {
return E.right(t.unknown);
}
default: {
return absurd(cursor);
}
}
})
);
您可以使用它从您描述的序列化中解码任意类型。一旦你验证了一个类型,你将拥有一个io-ts
类的运行时实例,这样你就可以使用它来验证传入的数据。在编译时,从使用递归 walker 代码在运行时解码的编解码器解码的每个值都是any
,因此您无法从静态类型检查中受益,但您仍然可以依赖验证。
这有点罗嗦,但我的意思是返回的值runtimeCodec.decode(someThing)
将Either<t.Errors, any>
在以下示例中。
来自这些解码的编解码器之一的验证只是断言数据处于指定的形状,但您在编译时对该数据的形状一无所知。
const someType: unknown = t.type({
a: t.string,
b: t.type({
foo: t.unknown
}),
c: t.union([t.undefined, t.literal("cat")])
});
console.log(recursivelyDecode(someType));
const assertRight = <E, T>(e: E.Either<E, T>): T => {
if (E.isLeft(e)) {
throw new Error("Failed to decode");
}
return e.right;
};
const runtimeCodec = assertRight(recursivelyDecode(someType));
const someThing: unknown = {
a: "123",
b: {
foo: new Date()
},
c: undefined
};
console.log(runtimeCodec.decode(someThing));
请注意,我没有添加案例,RecursiveType
因为我认为这可能涉及更多。有可能吗?另请注意,您需要为您创建的任何自定义类型添加案例,并且这些类型将需要一个唯一的_tag
属性才能在switch
. 我也在使用Object.fromEntries
,Object.entries
您可能需要更新"lib"
配置才能支持。(两种方法都有可用的 polyfill)。
我希望这是有帮助的。如果您在编译时需要实际类型,则需要从序列化的编解码器中生成某种代码。
编辑跟进:
有一张票可以为递归类型的序列化添加更好的支持,该票在撰写本文时仍处于开放状态。
还有这张票可以为类型模式的序列化添加更一般的支持。一旦这些都出去了,它将扩大您解决问题的容易程度。
推荐阅读
- go - Seed 和 Rand.Seed 的区别
- vba - 在 Access VBA 中将秒转换为时分秒格式
- c - 如何使用函数从列表中删除字符串?
- c++ - 关于 C++ enable_if_t;为什么这段代码不起作用(gcc 8.1,Ubuntu)
- python - 使用 selenium python 打开 USA Clothing 网站会将我重定向到 Europe Clothing 网站的主屏幕
- ios - 这个 kMSDataBaseType 错误在 xamarin 中是什么意思
- java - Java 8 Lambda 链接 - 类型安全强制
- json - 选择带有特殊字符的 JSON 值
- python - 使用另一个过程的 Python 过程不返回
- regex - 从正则表达式字符类中获取字符谓词`Char => Boolean`?