封装一个字典函数

2023 年 1 月 26 日 星期四(已编辑)
/
251
这篇文章上次修改于 2023 年 1 月 26 日 星期四,可能部分内容已经不适用,如有疑问可询问作者。

封装一个字典函数

完整代码: https://codesandbox.io/s/awesome-margulis-q2xibv?file=/src/index.ts

潜在的问题

在我们平常写代码的时候,可能需要对一个相同的数据进行各种转换,以便满足业务的需求,例如下方社交账号的例子:

const social = {
  bilibili: '291833916',
  netease: '345345345',
  weibo: '436453345',
}

if (social[type]) {
  // do something
}

这样可能可以满足当时的需求,但如果哪天业务变更,要求加入社交账号的 icon,你可能会改成这样:

export const social = [
  {
    name: 'bilibili',
    account: '291833916',
    icon: 'bilibili.svg',
  },
  {
    name: 'netease',
    account: '345345345',
    icon: 'netease.svg',
  },
  {
    name: 'weibo',
    account: '436453345',
    icon: 'weibo.svg',
  },
  // ...
];

但这样改的话,你之前的代码可能会报错。当然你也可能会重新创建一个对象来避免错误,但这样代码就越来越臃肿了。

又如果你以后需要用到所有社交账号的名称或者账号(下方的格式),你可能又需要通过 map 来获取到。

['bilibili', 'netease', 'weibo']
['291833916', '345345345', '436453345']

这样的代码又臃肿又混乱,而且还容易出错。因此我们完全可以封装一个工具函数,将一份定义转换成多种格式,从而实现如下效果。

{
  SOCIAL_TYPE_KEYS: [ 'bilibili', 'netease', 'weibo' ],
  SOCIAL_TYPE_VALUES: [ '291833916', '345345345', '436453345' ],
  SOCIAL_TYPE_KV: { bilibili: '291833916', netease: '345345345', weibo: '436453345' },
  SOCIAL_TYPE_VK: {
    '291833916': 'bilibili',
    '345345345': 'netease',
    '436453345': 'weibo'
  },
  SOCIAL_TYPE_MAP_BY_KEY: {
    bilibili: { key: 'bilibili', value: '291833916', icon: 'bilibili.svg' },
    netease: { key: 'netease', value: '345345345', icon: 'netease.svg' },
    weibo: { key: 'weibo', value: '436453345', icon: 'weibo.svg' }
  },
  SOCIAL_TYPE_MAP_BY_VALUE: {
    '291833916': { key: 'bilibili', value: '291833916', icon: 'bilibili.svg' },
    '345345345': { key: 'netease', value: '345345345', icon: 'netease.svg' },
    '436453345': { key: 'weibo', value: '436453345', icon: 'weibo.svg' }
  },
  SOCIAL_TYPE_KEY_MAP: {
    bilibili: { key: 'bilibili', value: '291833916', icon: 'bilibili.svg' },
    netease: { key: 'netease', value: '345345345', icon: 'netease.svg' },
    weibo: { key: 'weibo', value: '436453345', icon: 'weibo.svg' }
  },
  SOCIAL_TYPE_MAP: { bilibili: '291833916', netease: '345345345', weibo: '436453345' },
  SOCIAL_TYPE_LIST: [
    { key: 'bilibili', value: '291833916', icon: 'bilibili.svg' },
    { key: 'netease', value: '345345345', icon: 'netease.svg' },
    { key: 'weibo', value: '436453345', icon: 'weibo.svg' }
  ]
}

函数编写

我们可以编写如下函数:

export const social = [
  {
    key: 'bilibili',
    value: '291833916',
    icon: 'bilibili.svg',
  },
  {
    key: 'netease',
    value: '345345345',
    icon: 'netease.svg',
  },
  {
    key: 'weibo',
    value: '436453345',
    icon: 'weibo.svg',
  },
  // ...
]

function defineConstants(list) {
  return {
    SOCIAL_KEYS: list.map((item) => item.key),
    SOCIAL_KV: list.reduce(
      (map, item) => ({
        ...map,
        [item.key]: item.value,
      }),
      {},
    ),
    ....
  }
}
const data = defineConstants(social)
console.log(data);

//{
//  SOCIAL_KEYS: [ 'bilibili', 'netease', 'weibo' ],
//  SOCIAL_KV: { bilibili: '291833916', netease: '345345345', weibo: '436453345' }
//}

大致思路就是这样,但为了代码的通用性我们应当再传递一个参数,来当我们的前缀。

function defineConstants(list, namespace) {
  const prefix = namespace ? `${namespace}_` : ''
  return {
    [`${prefix}KEYS`]: list.map((item) => item.key),
    [`${prefix}KV`]: list.reduce(
      (map, item) => ({
        ...map,
        [item.key]: item.value,
      }),
      {},
    ),
  }
}

但这样的函数是没有类型提示的,因此我们需要使用 TypeScript 来进行类型定义:

interface IBaseDef {
  key: PropertyKey;
  value: string | number;
}

function defineConstants<T extends IBaseDef[], N extends string>(
  defs: T,
  namespace?: N,
) {
  const prefix = namespace ? `${namespace}_` : '';
  return {
    [`${prefix}KEYS`]: defs.map((item) => item.key),
  };
}

这样传入的参数便有了类型,我们还需要对返回值进行定义:

type ToProperty<Property extends string, N extends string = ""> = N extends ""
  ? Property
  : `${N}_${Property}`;

export type MergeIntersection<A> = A extends infer T
  ? { [Key in keyof T]: T[Key] }
  : never;

type ToKeyValue<T> = T extends readonly [infer A, ...infer B]
  ? B["length"] extends 0
    ? ToSingleKeyValue<A>
    : MergeIntersection<ToSingleKeyValue<A> & ToKeyValue<B>>
  : [];

type ToKeys<T> = T extends readonly [infer A, ...infer B]
  ? A extends {
      readonly key: infer K;
    }
    ? B["length"] extends 0
      ? [K]
      : [K, ...ToKeys<B>]
    : never
  : [];

type ToSingleKeyValue<T> = T extends {
  readonly key: infer K;
  readonly value: infer V;
}
  ? K extends PropertyKey
    ? {
        readonly [Key in K]: V;
      }
    : never
  : never;

function defineConstants<T extends readonly IBaseDef[], N extends string = "">(
  defs: T,
  namespace?: N
) {
  const prefix = namespace ? `${namespace}_` : "";
  return {
    [`${prefix}KEYS`]: defs.map((item) => item.key),
    [`${prefix}KV`]: defs.reduce(
      (map, item) => ({
        ...map,
        [item.key]: item.value,
      }),
      {}
    ),
  } as MergeIntersection<{
    [Key in ToProperty<"KV", N>]: ToKeyValue<T>;
  }> & {
    [Key in ToProperty<"KEYS", N>]: ToKeys<T>;
  };
}

TypeScript 涉及的知识点过多,就不再叙述了,最终代码可以看开头 codesandbox

参考

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...