TOP ⬆

Ts 踩坑记录-类型越大越好

Typescript的好处有很多,如维护方便、类型保护、代码提示良好等等 ,在Typescript的加持下,开发者往往只需要稍加注意一点细节就能写出更健全的代码,我们都知道完成业务功能是简单的,但将业务代码变得易维护和高鲁棒是不容易的,而在我看来Typescript恰恰就是打通这「代码最后一公里」的不二法门。

接下来,我会记录几个最近写代码时遇到的与 TS 相关的技巧(问题),以鞭策自己写好类型,拒绝anyscript

PS:以下提及的很多都是鸡肋的方法,毕竟javascript是如此的灵活,一个功能有 N 种实现方式,我所倡导的绝对不是一种「最佳实践」,只单纯希望能引起部分思考,增加对 TS 的认识。

小 Tips 之 Map 与 TS

Map 和 TS 的结合绝对是一种很好玩的存在!从Java走过来的我始终对泛型有着一种独特的欣赏,泛型的严谨对编程体验的提升是有很大帮助的,这种感受也延续到了typescript中,可以这么说使用Ts时,数据一旦被收集到带泛型的集合中魔法就自动产生了!

//	我们通常会这样定义一个带类型的Map
const mapWithType = new Map<K, V>();
//	key:string,value:{name:string,age:number}
const humanMap = new Map<string, { name: string; age: number }>();

这上面是我们平时使用 Map 时会遇到的基本用法,但有时候我们可能还会遇见key是已知的情况,常见的就是Map<key:string,value:Function>这种情况,此时如果我们将key转换成联合的字面量类型,那神奇的事件就发生了

type MyKeyType = "sleep" | "swing" | "run" | "walk";
type SuperMyKeyType = MyKeyType | 'super'		//	可拓展
type MyValueType = (who: Human) => void;
const humanMethodMap = new Map<MyKeyType, MyValueType>();

//	此时get()、set()等都有了非凡的代码提示,但其实只是相较Object多了另一种实现方式
const humanMethodMap.get()

//	当然你要这样实现那我也无话可说
const human = {
  sleep: () => { },
  run: () => { },
  swing: () => { },
  walk: () => { },
};
type GetKeyStringUnionType<T> = {
  [K in keyof T]: K
}[keyof T];
type HumanKey = getKeyStringUnionType<typeof human>;

什么?你问我为什么不用 Object 来放?因为有集合就用集合!这是俺们原教旨主义的坚持。

踩坑之 Type 尽量写大点

先上业务需求:当前有一个千分位分隔符的开关,需要添加一个功能「按印度进位制分隔」并将开关转换成选择框,方便日后拓展

先别着急写代码,这里的业务功能实现是很简单的,无非就是在当前的Numer(value).toLocaleString(■)中填入相对应的参数,我们真正应当考虑的是如何设计才更好的能向上兼容,毕竟历史的数据是以开关的方式保存的,为了更新数据,则一定需要重新映射的。

于是我们写出如下的选择框配置:

"separatorType": [
    {
      "name": "不使用分隔符",
      "value": "",
    },
    {
      "name": "千分位",
      "value": "en",
    },
    {
      "name": "印度进位制",
      "value": "en-in",
    }
  ]

我们只需要将false->'' & true->'en'即可完成兼容,但这真的最优吗?别忘了我们最终使用到的就是.toLocaleString(),如果我们能将 value 直接和的参数关键字对应不是更方便吗?于是考虑依旧将「不使用分隔符」时的 value 设成 false,我们就不用做复杂的字符串比对啦!我们可以这样写:

type SeparatorType = false | 'en' | 'en-in';
//	typeof getSeparator =  () => SeparatorType | undefined
const separator = getSeparator();
if (separator) {
	//	这里就展现出了我们设计的巧妙了
	//	业务中在这里对separator使用了string特有的方法如.startWith()等
	Number(value).toLocaleString(separator);
}

发现了吗?上面的巧妙之处就是将falseundefined都过滤了出去,在.toLocaleString()这里只会出现我们预期想要的值!鲁棒性、严谨性、逼格就因为一个小小的false就提升到如此地位,Ts 真是无敌啊!但?真的如此吗??

我们先接着看兼容方面的处理,处理是很简单的,首先获取到后端返回的 json,找到分隔符将 true 和 false 进行映射,如果是 undefined 则跳过,结果统一返回即可。做完兼容处理后的你快快乐乐地跑了一遍自测,跑了一遍代码校验,发现统统成功,于是你开心地将代码提交到了分支上,此时你的一位好同事拉取好你的代码后,打开了他的一个报表,恰好这个报表保存在另一个环境下并使用了一个怪异的组件而这个组件的历史数据中刚好又引用了你刚刚修改的分隔符,嘣~兼容处理失败,页面也报错了。只留下还剩十分钟就下班的你开始怀疑人生,这他妈到底是为什么????WHY?????? 人生的意义到底在哪里?

上面我们的问题到底出在了哪里?原来,在你的项目中某一个环境下配置了一份后端的脚本,这个脚本会覆盖部分组件的配置,其中就包括这个分隔符,其不会随着后端返回的 json 一起被传回,我们还是遗漏了部分组件的兼容处理,即此时的separator=truetrue上是没有string类型的方法的,因此页面崩溃了!这个锅说到底给typescript背是不合理的,问题总是出在人的身上,我们明明有如此强大的辅助工具,却还是经常因为各种原因而犯傻事,这才是我们应当反思的。

其实以上所提到的所有巧妙所有严谨其实都有一个大大大大前提:提前做好兼容,保证兼容处理后的数据要符合SeparatorType,这样才是真正万无一失的,毕竟这里只是编译层面的处理。但有时候这就如同绝对导体无法达到一样,我们也无法保证代码是百分之百正确,那将类型定义得如此“精确”就是不对的,因为总会有你疏忽的时候,特别是面临这样的复杂联合类型。在最开始我们将false改成boolean就可以避免出现这样的异常了,这就是我提倡的**“类型越大越好”**的原因,这样你真的吃不了亏!

后记

我们在使用Typescript的时候,也不能总是选择相信,尽量借用类型来多考虑一些细枝末节的东西

这里只是在项目中遇到的一两个小小问题,组内不规范的typescript已经让代码慢慢开始散发出一种腐烂的气味,any是 ts 代码中的病毒,它是会扩散的,在某处使用any省下的时间,后续可能需要花两至三倍去修复它带来的问题。经过各种锻炼的我现在在遇到any时,总会自觉使用各种各样的校验以确保此处有值,在一开始好好做好类型,这样的问题根本就不会发生!每次维护代码中遇到undefined这类本可以避免的错误,就好像手指被咬了一个大包一样难受,为什么连正确使用typescript都成为一件难事呢?我想这就是我不想在组内长留的原因吧,毕竟上面的所提及的事情是我的亲身经历…毕竟我连某些组件会由后端脚本处理都不知道…

为什么要去对技术认真的厂?

没参与业务前对大厂其实是很朦胧的,只知道大厂是一种不错的选择,现在才意识到其实大不大厂的无所谓,自己追求的其实是对技术的锤炼提升,与其说一定要去大厂,还不如说一定能要去开发流程规范、代码规范、尊重技术、喜欢code review、崇尚标准化和会搞开源的项目组,在大厂中能够加入这样的技术团队的概率会更高一些。讲真,遇到连typescript都不用心维护的项目,还是早点脱坑吧。