关于TS的类型提取

项目 TS 化,最大的难点还是在于如何补齐类型。

模块类型未导出

虽然通过增加@types/xxx 来引入缺失的模块类型已是基操,但是,深入使用后,往往会发现,即便是原生就提供了类型声明的模块,导出的类型也会出现不够用的情况。

举个基础的例子,webpack 仓库有 CompilerMultiCompiler 两种类型,在为 MultiCompiler 增加钩子的时候,遇到了这样一个问题:

compiler.hooks.done.tap("XXPlugin", (stats) => {  this.doSomething(stats);});

doSometing 方法需要声明 stats 的类型,通过编辑器的代码提示可以看到, MultiCompiler 的 tap 方法使用的为 MultiStats 类型,而在旧版的 webpack 中,只导出了 Stats,而没有导出 MultiStats(最新的 webpack 版本已经导出该类型)。

Hook<[MultiStats], void, UnsetAdditionalOptions>.tap(options: string | (TapOptions & {    name: string;}), fn: (args_0: MultiStats) => void): void

显然,如下的声明是不可取的:

private doSomething(stats: any) {    // do something}

那么,难道我们要自己声明一个 MultiStats 接口吗?这显然不合理。查找 webpack 的声明文件,笔者找到了一个细节。

declare interface CallbackFunction<T> {  (err?: Error, result?: T): any;}
declare class MultiCompiler {  // ...  run(callback: CallbackFunction<MultiStats>): void;  // ...}

webpack 的类型库导出了 MultiCompiler,而在 MultiCompiler 的 run 方法的参数中,使用到了一个 CallbackFunction<MultiStats>,而 CallbackFunction 实际上也是一个方法,它的第二个参数类型,就是我们所需的 MultiStats。既然有路子,何愁走不通,于是,就有了如下的声明方式。

export type MultiStats = NonNullable<  Parameters<Parameters<MultiCompiler["run"]>[0]>[1]>;

相信大家应该看过不少 typescript 高级类型 xxxx 的文章,看的时候心里可能是,卧槽,这个好牛逼,那个也好牛逼;但是是不是也会感觉,这么多骚操作,到底有何用,为什么我就看不进去呢?其实,在类型缺失、编写抽象库的时候,它们都是解决 any 的大杀器。先复习一波 ts 提供的基础工具类型:

// 处理Object的工具类interface IT {  key1: string;  key2: number;  key3: symbol;}
// Partial<T>: 全部属性变为可选type IPartial = Partial<IT>;// 等效于interface IPartialEqual {  key1?: string;  key2?: number;  key3?: symbol;}
// Required<T>: Partial的逆运算type IRequired = Required<IPartial>;// 等效于interface IRequiredEqual {  key1: string;  key2: number;  key3: symbol;}
// Pick<T, K extends keyof T>: 选出T的属性子集type IPick = Pick<IT, "key1" | "key3">;// 等效于interface IPickEqual {  key1: string;  key3: symbol;}// 和联合类型相关的工具类type U1 = string | number | symbol | boolean;type U2 = string | boolean | HTMLElement;
// Exclude<T, U> 从联合类型中排除U,跟差集类似type UExclude = Exclude<U1, U2>;// 等效于type UExcludeEqual = number | symbol;
// Extract<T, U> 从联合类型中取U的公共部分,跟交集类似type UExtract = Extract<U1, U2>;// 等效于type UExtractEqual = string | boolean;
// NonNullable<T> 从类型中排除掉 null/undefinedtype UNNString = NonNullable<string | null | undefined>; // 等效为 string

那么,敲黑板, Pick 是取出给出的 key,如果刚好是要排除给出的 key,应该怎么做?(PS: 事实上 ts 已自带了 Omit 类型)

// 解题思路// keyof T 可以获取类型T的属性,以联合类型的形式存在// Exclude 可以排除联合对象中的部分类型// 1 + 1 = ?type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

对于方法和对象,typescript 同样定义了一系列基础工具类

  • Parameters<T>: 获取方法的参数,返回为一个 Tuple

  • ConstructorParameters<T>: 同 Parameters,但是专门用于获取 constructor 的参数

  • ReturnType<T>:获取方法的返回值

再配合万能的 typeof,以及通过 ['property']获取属性类型的方式,我们就可以从任意链路提取类型了。再次回到 MultiStats

export type MultiStats = NonNullable<  Parameters<Parameters<MultiCompiler["run"]>[0]>[1]>;

即:取出 MultiCompiler 的 run 方法的第一个参数类型,再将该函数参数类型的第二个传参类型返回。

提升一下难度,如何获取下方代码中的 IDocument 类型?

export function getDocument(src: string): Promise<IDocument>;

通过 ReturnType 我们仅仅能获取一个 Promise<IDocument> 类型。此时就需要 infer 关键词出场了,顾名思义, infer 即为推断,配合 extends 语句,可以用来推断对应位置的类型:

type Awaited<T> = T extends PromiseLike<infer U> ? U : T;

通过 Awaited 类型,当 T 满足 PromiseLike<U> 的形式时,将会返回 U 类型,否则,则返回原类型。如此一来,就可以很方便的获取 IDocument 类型了。 infer 的功能十分强大,可以构成非常多的骚操作,事实上,内建的 ParametersConstructorParametersReturnType 均为 infer 的用例。

寻找 any 的真实类型

js 重构时,会遇到很多函数参数推断不出类型的情况,比如:

constructor XXXBar(options) {    this.opened = false;    this.bar = options.bar || null;    this.toggleButton = options.toggleButton || null;    this.findField = options.findField || null;    this.highlightAll = options.highlightAllCheckbox || null;    this.caseSensitive = options.caseSensitiveCheckbox || null;    this.entireWord = options.entireWordCheckbox || null;    this.findMsg = options.findMsg || null;    this.findResultsCount = options.findResultsCount || null;    this.findPreviousButton = options.findPreviousButton || null;    this.preArrow = options.preArrow || null;    this.findNextButton = options.findNextButton || null;    this.nextArrow = options.nextArrow || null;    this.closeButton = options.closeButton || null;    this.eventBus = eventBus;    this.l10n = l10n;}

bar 是啥? toggleButton 应该是个控件,但是是什么类型呢? findMsg 又是啥,字符串吗? caseSensitive 看着像个布尔值,为什么赋值后方是个 caseSensitiveCheckbox ? 是不是要祭出 any 大法?其实不然。对于这种没办法从代码中推断的东西,我们可以从调试中来获取,运行代码,打上一个断点:

图片

一切豁然开朗,原来 options 传入的是一组 dom 节点(PS:虽然看到这个结果的第一反应是,这种文件肯定是要删掉重写的)。所以,没有一个断点解决不了的事情,解决不了,那就打两个。如果像笔者一样懒惰,不喜欢一个个查询,也可以在增加一些简单的工具函数,快速的把类型打出来:

(window as any)._getTypes = (target: any) => {  Object.keys(target).forEach((key) => {    const val = target[key];    if (typeof val !== "object") {      console.log(`${key}: ${typeof val}`);    } else {      console.log(`${key}: ${val.constructor.name}`);    }  });};

配合断点使用,效果如下:

图片

一些建议

在重构的过程中,笔者个人有几个原则:

  1. 在无法确定变量是否可能为空的情况下,类型默认可为空。此时,IDE 和编译器会在可能为空的位置进行提示和报错,可以更好的整理清楚变量的使用逻辑,从而确定这个变量是否有为空的可能性,往往还能修复 js 代码中某些场景未判空的 bug。

  2. 成员方法和变量优先声明为 private,编译报错了再改为 public。这样能最大程度一次就改好变量/方法的修饰符,还有利于发现未被使用的函数。

  3. 在单个模块 ts 化完成前,尽可能减少对变量/方法名的调整,因为 js 的特点,编译器可能无法很好的帮助你完成变量/方法名的改名,导致埋下逻辑错误,后期发现时,排查将会变得十分困难。

总结

纵使搭配 eslint 规则和种种限制,JS 的弱类型和灵活性依旧容易让代码野蛮生长。也真是因此,项目的 TS 化过程中,切忌贪图一时爽快,大量使用 any 和 public,为项目的可持续发展埋上核弹。本回手记就暂时聊到这儿,有错误的地方欢迎各位大佬指正。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值