使用元组定义枚举类型转换

64 阅读3分钟

多个类型之间的两两转换

enum TopicGql {
  TodoApproval = "TodoApproval",
  DoneApproval = "DoneApproval",
  InitiatedApproval = "InitiatedApproval",
  UnreadNotice = "UnreadNotice",
  ReadNotice = "ReadNotice",
  Shared = "Shared",
}
enum TopicB2e {
  TodoApproval = 1,
  DoneApproval = 2,
  InitiatedApproval = 3,
  UnreadNotice = 17,
  ReadNotice = 18,
  Shared = 33,
}

...更多类型

常见的几种类型转换

基于 Map

export const topicGqlToB2eMap = {
    [TopicGql.TodoApproval]: [TopicB2e.TodoApproval],
    [TopicGql.DoneApproval]: [TopicB2e.DoneApproval],
    [TopicGql.InitiatedApproval]: [TopicB2e.InitiatedApproval],
    [TopicGql.UnreadNotice]: [TopicB2e.UnreadNotice],
    [TopicGql.ReadNotice]: [TopicB2e.ReadNotice],
    [TopicGql.Shared]: [TopicB2e.Shared],
}

基于函数封装: if-else 或 switch-case

export const topicGqlToB2e = (value: TopicGql) => {
    if (value === TopicGql.TodoApproval) return TopicB2e.TodoApproval;
    if (value === TopicGql.DoneApproval) return TopicB2e.DoneApproval;
    if (value === TopicGql.InitiatedApproval) return TopicB2e.InitiatedApproval;
    if (value === TopicGql.UnreadNotice) return TopicB2e.UnreadNotice;
    if (value === TopicGql.ReadNotice) return TopicB2e.ReadNotice;
    if (value === TopicGql.Shared) return TopicB2e.Shared;
}

当我们开始使用元组

const topicTupleList: [TopicGql, TopicB2e, 更多类型][] = [
  [TopicGql.DoneApproval, TopicB2e.DoneApproval, 更多类型],
  [TopicGql.InitiatedApproval, TopicB2e.InitiatedApproval, 更多类型],
  [TopicGql.ReadNotice, TopicB2e.InitiatedApproval, 更多类型],
  [TopicGql.Shared, TopicB2e.Shared, 更多类型],
  [TopicGql.TodoApproval, TopicB2e.TodoApproval, 更多类型],
  [TopicGql.UnreadNotice, TopicB2e.UnreadNotice, 更多类型],
];

数组 vs 元组

官方文档: www.typescriptlang.org/docs/handbo…

  • 数组 Array 是 js 中的一种数据类型.

  • 元组 Tuple 本质上也是数组. 但在 ts 语境下, 我们需要与数组进行区分

从定义的元组中实现类型转换

export const topicGqlToB2e = (value: TopicGql) => 
    topicTupleList.find((tuple) => tuple[0] === value)?.[1];

优势:

  • 对于两种以上枚举 (如国际化语言 zh-cn / zh_CN / zh) 之前互相转换

缺点:

  • 仍然要分别实现转换方法

  • 实现中的元组下标容易写错

高阶函数封装 + 类型定义

将二维数组想象成数据库表, 我们可以先对表头进行类型定义.

enum TopicStyleIndex {
  Gql = 0,
  B2e = 1,
}

type TopicTuple = [TopicGql, TopicB2e];

创建高阶函数, 利用参数推导类型

const createTopicGen = 
<From extends TopicStyleIndex, To extends TopicStyleIndex>(from: From, to: To ) => {
  return (value: TopicTuple[From]): TopicTuple[To] | undefined =>
    topicTupleList.find((tuple) => tuple[from] === value)?.[to];
};

export const topicGqlToB2e = createTopicGen(
  TopicStyleIndex.Gql,
  TopicStyleIndex.B2e
);

export const topicB2eToGql = createTopicGen(
  TopicStyleIndex.B2e,
  TopicStyleIndex.Gql
);

性能优化

转换查询仅对二维数组做一层(行)遍历, 二层(列)使用下标查询, 一般不会有性能问题.

  • 除非你的枚举可取值非常多, 从而外层(行)数组非常长
  • 除非你的转换操作非常频繁

并不包括枚举类型非常多, 因为内层(列)使用下标查询.

基于索引的 map 对性能来说是必要的 (牺牲空间换时间), 可以在转换方法调用时实时创建 map

将二维数组想象成数据库表的话, 可以把创建 map 想象成建立索引

const createTopicGen = <
  From extends TopicStyleIndex,
  To extends TopicStyleIndex
>(
  from: From,
  to: To
) => {
  const map = new Map();  // 可以使用 js 对象代替, 但 js 对象的 key 只能是字符串
  const nil = {}; // 使用唯一值表示 undefined, 防止与枚举值中的 undefined 冲突
  return (value: TopicTuple[From]): TopicTuple[To] | undefined => {
    const ret = map.get(value);
    if (!ret) {
      const _ret = topicTupleList.find((tuple) => tuple[from] === value)?.[to];
      map.set(value, _ret);
      return _ret;
    }
    if (ret === nil) {
      return undefined;
    }
    return ret;
  };
};

与 Map 配合使用

没有银弹, 元组也不可能适用于所有情况

一些场景下可能还是需要 函数封装的 if-else 或 switch-case

  • 来源类型的多个值要转成目标类型的一个值, 或来源类型的一个值可以转成目标类型的多个值

也会有一些场景需要元组和 Map 来配合使用.

  • 如果只存在单向转换(只有一个索引字段), 建议还是定义 map, 例如通过 key 获取一个对象, 获取一个 icon 啥的.

  • 如果存在多个索引字段, 可以将元组与 map 配合使用, 元组仅作为索引字段之间的转换, 指定一个 key 用来在 map 中查找其他值.

Map 推荐使用场景

例如:

idnameemailavatar
xxxxxxxx@xxx.xxxxxxx

如果只是通过 id 转换到其他字段, 可以定义 map

{
    [id]: { name, email, avatar }
}

而不建议 (非要用也行, 不就是不好看) 元组, 因为 name, avatar 不可被索引.

元组 + Map 使用场景

如前文所示, 如果也需要 email 来查找 name 和 avatar,

这个时候, 可以先创建 [id, email][] 的元组定义

就可以通过 email 先找到 id, 再通过 Map 中的索引 id 来找到其它字段.