首页 > 解决方案 > 如何序列化 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中恢复对象

标签: typescript

解决方案


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.fromEntriesObject.entries您可能需要更新"lib"配置才能支持。(两种方法都有可用的 polyfill)。

我希望这是有帮助的。如果您在编译时需要实际类型,则需要从序列化的编解码器中生成某种代码。

编辑跟进:

有一张票可以为递归类型的序列化添加更好的支持,该票在撰写本文时仍处于开放状态。

还有这张票可以为类型模式的序列化添加更一般的支持。一旦这些都出去了,它将扩大您解决问题的容易程度。


推荐阅读