Google员工吐槽TypeScript:我觉得你的类型检查不太好

近日名为 Evan Martin 的 Google 员工在 TypeScript 的 GitHub repo 中发表了对 TypeScript 的“吐槽”(就是提了一个 issue),说吐槽可能不太合适,准确来说是对 TypeScript 3.5 的使用反馈。

Google员工吐槽TypeScript:我觉得你的类型检查不太好

虽然 TypeScript 3.5 发布已有三个月(最新稳定版 3.6 已于上月月底发布),但 Google 开发团队最近才升级至 3.5 版本。使用一段时间后,开发者觉得不吐不快,于是便有了这篇质量颇高的使用反馈。是的,这里说的项目正是被众人使用的 Google —— 那个只有一个代码仓库且拥有数十亿行代码的 Google。

背景

开发团队面对的项目是拥有数十亿行代码的 Google,在团队内部,所有成员使用的是同一版本的 TypeScript 和同一组跨所有平台的编译器标记(compiler flag),如需升级,成员会协助为所有人同时升级这些标记。

Evan 说到,他和大家一样会期望 TypeScript 的新版本升级能带来一些改进。例如,Evan 表示自己希望并欢迎对标准库进行改进,即便这可能意味着需要从代码库中删除类似但不兼容的定义。但团队发现此次升级至 TypeScript 3.5 带来的额外工作量要比此前的升级多得多。

Evan 认为 3.5 版本中有三个主要变化让此次升级变得尤其艰难,他相信这些变化的大多数是有其目标的,并且旨在改进类型检查,但他也认为 TypeScript 团队所理解的类型检查始终只是在安全与效率之间权衡。

Evan 希望这份大型代码库的 TypeScript 使用反馈能帮助 TypeScript 团队更好地评估未来类似的情况,并提供一些建议。

下面看看 Evan 说的 3.5 版本给团队带来影响的三个主要变化。

泛型的隐式默认值(Implicit default for generics)

此项特性属于 3.5 版本中的破坏性变化,Evan 认为这里导致出现问题的原因是代码的泛型与代码所做的工作并无相关。例如,假设有一些具有 Promise 解析的代码,但不关心 Promise 要解析的值:

  1. function dontCarePromise() { 
  2.   return new Promise((resolve) => { 
  3.     resolve(); 
  4.   }); 

由于泛型是未绑定的,在 3.4 中为 Promise<{}> 的代码在 3.5 中就会变为 Promise。如果此函数的使用者在任意地方写下了这种类型的 Promise:

  1. const myPromise: Promise<{}> = dontCarePromise(); 

这将会导致出现类型错误。

除此之外,还有一种被称为“仅返回泛型(return-only generics)”的模式,这种情况下,泛型函数仅在返回类型中使用它的任意模式。这里导致的问题是,会出现很多类型推导意外。例如,在只返回泛型的情况下,有如下的代码:

  1. expectsString(myFunction()); 

可以按以下的方式合法重构:

  1. const x = myFunction(); 
  2. expectsString(x); 

但最后发现,这是行不通的。

布尔过滤器 filter(Boolean)

TypeScript 3.5 更改了Boolean函数的类型,该函数会强制赋值给boolean,从

  1. function Boolean(value?: any): boolean; 

变为

  1. function Boolean<T>(value?: T): boolean; 

两者看起来可能非常相似。但试想一下,一个函数接受了一个谓词并返回一个数组过滤器,并像上面的代码一样使用:

  1. function filter<T>(predicate: (t: T) => boolean): (ts: T[]) => T[]; 
  2. const myFilter = filter(Boolean); 

在 3.4 版本中,根据定义,T 从 any 变为myFilter,并成为一个由 any[]到 any[] 的函数。但在 3.5 版本中,T 只保留了泛型。

集合(Set)

在 TypeScript 3.4 中,下面的代码:

  1. const s = new Set(); 

会返回一个 Set。但 TypeScript 3.5 出现了一个变更,使得 lib.es2015.iterable.d.ts 具有移除 any 的效果,然后导致泛型改变上面的描述,并将类型推导为 unknown。

这种变化最终很难修复,因为最终的类型错误有时与实际问题相差甚远。例如,在如下代码中:

  1. class C { 
  2.   gather() { 
  3.     let s = new Set(); 
  4.     s.add('hello'); 
  5.     return s; 
  6.   } 
  7.   use(s: string[]) { … } 
  8.   demo() { 
  9.     this.use(Array.from(this.gather())); 
  10.   } 

我们会收到关于 Array.from 类型错误的提示,但实际需要修复的是 new Set()。