完整代码: https://codesandbox.io/s/awesome-margulis-q2xibv?file=/src/index.ts
潜在的问题
在我们平常写代码的时候,可能需要对一个相同的数据进行各种转换,以便满足业务的需求,例如下方社交账号的例子:
typescriptconst social = { bilibili: '291833916', netease: '345345345', weibo: '436453345', } if (social[type]) { // do something }
这样可能可以满足当时的需求,但如果哪天业务变更,要求加入社交账号的 icon,你可能会改成这样:
typescriptexport const social = [ { name: 'bilibili', account: '291833916', icon: 'bilibili.svg', }, { name: 'netease', account: '345345345', icon: 'netease.svg', }, { name: 'weibo', account: '436453345', icon: 'weibo.svg', }, // ... ];
但这样改的话,你之前的代码可能会报错。当然你也可能会重新创建一个对象来避免错误,但这样代码就越来越臃肿了。
又如果你以后需要用到所有社交账号的名称或者账号(下方的格式),你可能又需要通过 map 来获取到。
typescript['bilibili', 'netease', 'weibo'] ['291833916', '345345345', '436453345']
这样的代码又臃肿又混乱,而且还容易出错。因此我们完全可以封装一个工具函数,将一份定义转换成多种格式,从而实现如下效果。
typescript{ 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' } ] }
函数编写
我们可以编写如下函数:
typescriptexport 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' } //}
大致思路就是这样,但为了代码的通用性我们应当再传递一个参数,来当我们的前缀。
typescriptfunction 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 来进行类型定义:
typescriptinterface 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), }; }
这样传入的参数便有了类型,我们还需要对返回值进行定义:
typescripttype 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。