—— 一次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 & DefaultConfigSpecial
type GlobalConfig = GlobalConfigBase & GlobalConfigSpecial
const 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即可。