—— 一次CR导致的秃头
问题
在开发中会有一些全局配置,例如选择器的Options、权限等都是通过一个接口,从配置信息里直接获取的。
而且全局的default_config是随时可能新增的,而我们每次新增一个属性,都需要改动三处,且绝大多数改动完全一致。
虚拟代码大概如下所示:
interface Item {label: string;value: string;}interface DefaultConfig{str1: string;number1:number;a:Item;b:Item;c:Item;// ………… * 此处省略20个y:Item}const getGlobalConfig = (config: DefaultConfig): GlobalConfig => ({str1_str: config.str1,number1_str:`${config.number1}`,a_opt:getOption(config.a),b_opt:getOption(config.b),c_opt:getOption(config.c),// ………… * 此处省略20个y_opt:getOption(config.y)})const getOption=(config:Item):Option=>({...config,default:'test'})interface Option {label: string;value: string;default: string;}interface GlobalConfig{str1_str: string;number1_str:string;a_opt:Option;b_opt:Option;c_opt:Option;// ………… * 此处省略20个y_opt:Option}
整个流程:
通过一个 DefaultConfig 定义了全局配置接口的返回值的类型
发现初始配置项不能满足直接使用,于是通过一个 getGlobalConfig 函数处理
又通过 GlobalConfig 定义了 getGlobalConfig 函数返回值的类型
从上面的代码中我们可以看到,每次配置项新增一个的时候,接口返回值类型DefaultConfig、转换函数getGlobalConfig及函数返回值类型GlobalConfig都需要有对应改动。
例: 返回值增加 z 属性,则DefaultConfig中增加z:Item、getGlobalConfig中增加z_opt:getOption(config.z)、 GlobalConfig中增加 z_opt:Option
~~我mentor让我优化下 ~~多次重复操作拉低了开发效率且很容易漏改出错,所以对这部分的写法做一个优化。
目标
- 必须得保留类型,不然这么大的对象开发中没有提示肯定不行。(有道理,没提示简直噩梦)
- 必须少写,不然每次改动三个地方浪费时间还容易出错。 (有道理,每次改三个
也还行简直噩梦)
综合来看目标就是,只写一处,三处生效。
难点
三处里有两处是类型定义 你们在想啥? 类型定义啊,你不定义哪来的类型?
最后一处是js对象定义 ? 在想啥? 在js里你要拿一个TS的interface当变量生成对象?
结论: 做不了!
定一个小目标:减少一处类型定义
前几天组内伙伴分享了一篇文章:https://zhuanlan.zhihu.com/p/426966480
我只看懂了第一句:众所周知,TypeScript 是图灵完备的 (众所周知??那我怎么才知道?)
而且我们在开发当中经常用到映射类型 例如: Partial<T> ,他的源码就是:
type Partial<T> = {[P in keyof T]?: T[P];};
诶,他写逻辑了,你们看他是不是写逻辑了!
结合两点来看,TS本身确实可以通过自身的语法来实现从一个类型转换成另一个类型。
从上面的代码中看出,想要实现的逻辑很简单,把 DefaultConfig 里面所有的 Item 类型的属性找出来,之后给 GlobalConfig 一个 属性,类型定为 Option 。写一个自定义的映射类型,实现这个逻辑就能完成这个小目标了。
在TS的新官网中就有有关类型编程的章节: Documentation - Creating Types from Types
除此之外依旧借鉴了成熟库中的实现及语法: https://github.com/piotrwitek/utility-types
实现:
- 将
DefaultConfig类型中所有的Item类型的过滤出来 - 遍历过滤出来的类型,将key转换为 key+’opt’字符串,将类型设置为
Option类型,输出一个新类型 - 用新类型和几个特殊值合并输出目标类型
GlobalConfig
代码如下:
// 过滤DefaultConfig并转换为setGlobalConfig通用样式 --- 高阶函数??type GlobalConfigBase = SetGlobalConfigBase<PickByValueExact<DefaultConfig, Item>>;// 非典型的单独维护type GlobalConfigSpecial = {str1_str: string;number1_str:string;};type GlobalConfig = GlobalConfigBase & GlobalConfigSpecial// 根据类型过滤type PickByValueExact<T, ValueType> = Pick<T,{[Key in keyof T]-?: [ValueType] extends [T[Key]]? [T[Key]] extends [ValueType]? Key: never: never;}[keyof T]>;// 将一个类型转化为GlobalConfig通用样式type SetGlobalConfigBase<T> = {[K in keyof T as `${K & string}_opt`]: Option;};
这样一段代码就能替代每次都手动修改的GlobalConfig了,鼠标移上去看下一 GlobalConfigBase的类型:
目测也是符合预期的,而且getGlobalConfig 函数也没报错。好了好了结束了,剩下的那部分 …… 下次一定
下面我们进行函数部分优化。
最终:减少globalConfigVM函数内部重复
探索了一上午怎么能把 在js中判断 type,探索无果。因为本身让类型嵌入运行逻辑这个事情就是合理的,也是不被ts允许的。 然后看到一篇平平无奇的文章: Typescript使用字符串联合类型代替枚举类型
里面的主体内容属实没什么用,看标题就知道,所以大家就不用点开看了。 但是里面有一句话给了我提示:
enum类型了引入了 JavaScript 没有的数据结构(编译成一个双向 map),入侵了运行时,与 TypeScript 宗旨不符。
对啊,枚举,它既是运行时代码,又是一种类型,在TS里可以用 typeof 解析的呀。就用它做基础数据,在类型层用自定义映射类型转换出 Data 和 transformData ,在transformer层用它的属性来过滤需要执行 buildOptions函数的属性。 以后只要是通用格式就只要维护这个 enum 就可以了啊。
结果:
// 维护一个初始类型为Item的枚举enum BaseEnum {a = 1,b,c,// ………… * 此处省略20个y}// 获取全部类型为Item的key值type BaseKey = keyof typeof BaseEnum// 构建DefaultConfig中类型为Item的集合type SetDefaultConfigBase<T extends string> = {[Key in T]:Item}// 构建GlobalConfig中类型为option的集合type SetGlobalConfigBase<T extends string> = {[Key in T as `${Key}_opt`]: Option;};// DefaultConfig中类型为Item的集合type DefaultConfigBase = SetDefaultConfigBase<BaseKey>// DefaultConfig中类型为Item的集合type GlobalConfigBase = SetGlobalConfigBase<BaseKey>// 非典型的单独维护type DefaultConfigSpecial = {str1: string;number1:number;};type GlobalConfigSpecial = {str1_str: string;number1_str:string;};type DefaultConfig = DefaultConfigBase & DefaultConfigSpecialtype GlobalConfig = GlobalConfigBase & GlobalConfigSpecialconst getGlobalConfig = (config: DefaultConfig): GlobalConfig => ({...getGlobalConfigBase(config,BaseEnum),str1_str: config.str1,number1_str:`${config.number1}`})const getGlobalConfigBase = (config: DefaultConfig, baseKey):GlobalConfigBase =>Object.keys(config)?.filter((key: string) => baseKey[key]).reduce((prev, current) => {const obj = { ...prev };obj[`${current}_opt`] = getOption(config[current]);return obj;}, {});const getOption=(config:Item):Option=>({...config,default:'test'})
至此GlobalConfig就简化到一处BaseEnum维护,后续增加新的Item选项,只需要在定义好的 enum 中添加一个key即可。
