—— 一次CR导致的秃头

问题

在开发中会有一些全局配置,例如选择器的Options、权限等都是通过一个接口,从配置信息里直接获取的。
而且全局的default_config是随时可能新增的,而我们每次新增一个属性,都需要改动三处,且绝大多数改动完全一致。
虚拟代码大概如下所示:

  1. interface Item {
  2. label: string;
  3. value: string;
  4. }
  5. interface DefaultConfig{
  6. str1: string;
  7. number1:number;
  8. a:Item;
  9. b:Item;
  10. c:Item;
  11. // ………… * 此处省略20个
  12. y:Item
  13. }
  14. const getGlobalConfig = (config: DefaultConfig): GlobalConfig => ({
  15. str1_str: config.str1,
  16. number1_str:`${config.number1}`,
  17. a_opt:getOption(config.a),
  18. b_opt:getOption(config.b),
  19. c_opt:getOption(config.c),
  20. // ………… * 此处省略20个
  21. y_opt:getOption(config.y)
  22. })
  23. const getOption=(config:Item):Option=>({
  24. ...config,
  25. default:'test'
  26. })
  27. interface Option {
  28. label: string;
  29. value: string;
  30. default: string;
  31. }
  32. interface GlobalConfig{
  33. str1_str: string;
  34. number1_str:string;
  35. a_opt:Option;
  36. b_opt:Option;
  37. c_opt:Option;
  38. // ………… * 此处省略20个
  39. y_opt:Option
  40. }

整个流程:
通过一个 DefaultConfig 定义了全局配置接口的返回值的类型
发现初始配置项不能满足直接使用,于是通过一个 getGlobalConfig 函数处理
又通过 GlobalConfig 定义了 getGlobalConfig 函数返回值的类型

从上面的代码中我们可以看到,每次配置项新增一个的时候,接口返回值类型DefaultConfig、转换函数getGlobalConfig及函数返回值类型GlobalConfig都需要有对应改动。
例: 返回值增加 z 属性,则DefaultConfig中增加z:ItemgetGlobalConfig中增加z_opt:getOption(config.z)GlobalConfig中增加 z_opt:Option

~~我mentor让我优化下 ~~多次重复操作拉低了开发效率且很容易漏改出错,所以对这部分的写法做一个优化。

目标

  1. 必须得保留类型,不然这么大的对象开发中没有提示肯定不行。(有道理,没提示简直噩梦)
  2. 必须少写,不然每次改动三个地方浪费时间还容易出错。 (有道理,每次改三个 也还行 简直噩梦)
    综合来看目标就是,只写一处,三处生效。

难点
三处里有两处是类型定义 你们在想啥? 类型定义啊,你不定义哪来的类型?
最后一处是js对象定义 ? 在想啥? 在js里你要拿一个TS的interface当变量生成对象?

结论: 做不了!

定一个小目标:减少一处类型定义

前几天组内伙伴分享了一篇文章:https://zhuanlan.zhihu.com/p/426966480
我只看懂了第一句:众所周知,TypeScript 是图灵完备的 (众所周知??那我怎么才知道?)

而且我们在开发当中经常用到映射类型 例如: Partial<T> ,他的源码就是:

  1. type Partial<T> = {
  2. [P in keyof T]?: T[P];
  3. };

诶,他写逻辑了,你们看他是不是写逻辑了!

结合两点来看,TS本身确实可以通过自身的语法来实现从一个类型转换成另一个类型。

从上面的代码中看出,想要实现的逻辑很简单,把 DefaultConfig 里面所有的 Item 类型的属性找出来,之后给 GlobalConfig 一个 属性,类型定为 Option 。写一个自定义的映射类型,实现这个逻辑就能完成这个小目标了。

在TS的新官网中就有有关类型编程的章节: Documentation - Creating Types from Types
除此之外依旧借鉴了成熟库中的实现及语法: https://github.com/piotrwitek/utility-types

实现:

  1. DefaultConfig 类型中所有的 Item 类型的过滤出来
  2. 遍历过滤出来的类型,将key转换为 key+’opt’字符串,将类型设置为 Option 类型,输出一个新类型
  3. 用新类型和几个特殊值合并输出目标类型 GlobalConfig
    代码如下:
  1. // 过滤DefaultConfig并转换为setGlobalConfig通用样式 --- 高阶函数??
  2. type GlobalConfigBase = SetGlobalConfigBase<PickByValueExact<DefaultConfig, Item>>;
  3. // 非典型的单独维护
  4. type GlobalConfigSpecial = {
  5. str1_str: string;
  6. number1_str:string;
  7. };
  8. type GlobalConfig = GlobalConfigBase & GlobalConfigSpecial
  9. // 根据类型过滤
  10. type PickByValueExact<T, ValueType> = Pick<
  11. T,
  12. {
  13. [Key in keyof T]-?: [ValueType] extends [T[Key]]
  14. ? [T[Key]] extends [ValueType]
  15. ? Key
  16. : never
  17. : never;
  18. }[keyof T]
  19. >;
  20. // 将一个类型转化为GlobalConfig通用样式
  21. type SetGlobalConfigBase<T> = {
  22. [K in keyof T as `${K & string}_opt`]: Option;
  23. };

这样一段代码就能替代每次都手动修改的GlobalConfig了,鼠标移上去看下一 GlobalConfigBase的类型:
image.png
目测也是符合预期的,而且getGlobalConfig 函数也没报错。
好了好了结束了,剩下的那部分 …… 下次一定

下面我们进行函数部分优化。

最终:减少globalConfigVM函数内部重复

探索了一上午怎么能把 在js中判断 type,探索无果。因为本身让类型嵌入运行逻辑这个事情就是合理的,也是不被ts允许的。 然后看到一篇平平无奇的文章: Typescript使用字符串联合类型代替枚举类型
里面的主体内容属实没什么用,看标题就知道,所以大家就不用点开看了。 但是里面有一句话给了我提示:

enum类型了引入了 JavaScript 没有的数据结构(编译成一个双向 map),入侵了运行时,与 TypeScript 宗旨不符。

对啊,枚举,它既是运行时代码,又是一种类型,在TS里可以用 typeof 解析的呀。就用它做基础数据,在类型层用自定义映射类型转换出 Data 和 transformData ,在transformer层用它的属性来过滤需要执行 buildOptions函数的属性。 以后只要是通用格式就只要维护这个 enum 就可以了啊。
结果:

  1. // 维护一个初始类型为Item的枚举
  2. enum BaseEnum {
  3. a = 1,
  4. b,
  5. c,
  6. // ………… * 此处省略20个
  7. y
  8. }
  9. // 获取全部类型为Item的key值
  10. type BaseKey = keyof typeof BaseEnum
  11. // 构建DefaultConfig中类型为Item的集合
  12. type SetDefaultConfigBase<T extends string> = {
  13. [Key in T]:Item
  14. }
  15. // 构建GlobalConfig中类型为option的集合
  16. type SetGlobalConfigBase<T extends string> = {
  17. [Key in T as `${Key}_opt`]: Option;
  18. };
  19. // DefaultConfig中类型为Item的集合
  20. type DefaultConfigBase = SetDefaultConfigBase<BaseKey>
  21. // DefaultConfig中类型为Item的集合
  22. type GlobalConfigBase = SetGlobalConfigBase<BaseKey>
  23. // 非典型的单独维护
  24. type DefaultConfigSpecial = {
  25. str1: string;
  26. number1:number;
  27. };
  28. type GlobalConfigSpecial = {
  29. str1_str: string;
  30. number1_str:string;
  31. };
  32. type DefaultConfig = DefaultConfigBase & DefaultConfigSpecial
  33. type GlobalConfig = GlobalConfigBase & GlobalConfigSpecial
  34. const getGlobalConfig = (config: DefaultConfig): GlobalConfig => ({
  35. ...getGlobalConfigBase(config,BaseEnum),
  36. str1_str: config.str1,
  37. number1_str:`${config.number1}`
  38. })
  39. const getGlobalConfigBase = (config: DefaultConfig, baseKey):GlobalConfigBase =>
  40. Object.keys(config)
  41. ?.filter((key: string) => baseKey[key])
  42. .reduce((prev, current) => {
  43. const obj = { ...prev };
  44. obj[`${current}_opt`] = getOption(config[current]);
  45. return obj;
  46. }, {});
  47. const getOption=(config:Item):Option=>({
  48. ...config,
  49. default:'test'
  50. })

至此GlobalConfig就简化到一处BaseEnum维护,后续增加新的Item选项,只需要在定义好的 enum 中添加一个key即可。