G2 升级依赖 d3-array type 报错:@types 不完全遵循 Semantic Versioning ?

90 阅读5分钟

G2 是蚂蚁集团 AntV 团队开源的企业级可视化框架,它的线上 CI 在某一个时间突然就挂了:

image.png

定位问题

CI 暴露的问题很明显:是类型相关的问题,但是问题是为啥突然类型报错了?难道是依赖升级导致的?

看了看报错信息,都和 group 这个函数有关,而这个函数是从 d3-array 里面导入的:

import { group } from 'd3-array';

然后在 npm 上面一看,@tyes/d3-array 这个库果然发布了一个新的版本:3.0.7。然后深入定位问题,发现是这个 PR 引入了 Breaking Change:

// 之前
const groups = group<TObject, Tkey1, Tkey2>(data, d => d.key1, d => d.key2);

// 现在
const groups = group<TObject, [Tkey1, Tkey2]>(data, d => d.key1, d => d.key2);

可以发现 group 的泛型发生了变化:之前是类似 rest 参数,现在是一个数组。

解决办法

当时还有别的事,所以就暂时锁定了 @types/d3-array,之后找个时间去代码里面升级。

{
  "devDependencies": {
    "@types/d3-array": "3.0.5",
  }
}

问题

虽然问题解决了,但是我还是有两个问题:

  • 为啥一个这么广泛使用的库,会违反 Semantic Versioning?
  • 新的范型真的比老的范型好吗?

第一个问题进一步解释就是:根据 Semantic Versioning,为了向后兼容,只有大版本的升级才能引入 Breaking Change,比如从 G2 从 4.x 升级到 5.x 就有很多 Breaking Changes。但是 @types/d3-array 从 3.0.5 升级到 3.0.7 只是一个 patch 版本,理论上不应该存在 Breaking Change 的。

第二个问题进一步解释就是:我觉得老的泛型更好,和 d3-array 中 group 函数的 JS 函数签名更加一致:

import { group } from 'd3-array';

const data = [
  {name: "jim",   amount: "34.0",   date: "11/12/2015"},
  {name: "carl",  amount: "120.11", date: "11/12/2015"},
  {name: "stacy", amount: "12.01",  date: "01/04/2016"},
  {name: "stacy", amount: "34.05",  date: "01/04/2016"}
]

// 我们是这样使用该方法的:
group(data, d => d.name, d => d.date);

// 而不是如下使用:
group(data, [d => d.name, d => d.date]);

group 这个方法会根据指定的 keys 对可迭代对象 data 进行聚合,上面的代码输出如下:

Map(3) {
  "jim" => Map(1) {
    "11/12/2015" => Array(1)
  }
  "carl" => Map(1) {
    "11/12/2015" => Array(1)
  }
  "stacy" => Map(1) {
    "01/04/2016" => Array(2)
  }
}

其实引入这个 Breaking Change 的原因是:group 方法本身是可以指定无限个 key 的,但是之前泛型通过重载只支持了指定3个 key 的类型,下面是这个 PR 之前的代码:

export function group<TObject, TKey>(iterable: Iterable<TObject>, key: (value: TObject) => TKey): InternMap<TKey, TObject[]>;

export function group<TObject, TKey1, TKey2>(
    iterable: Iterable<TObject>,
    key1: (value: TObject) => TKey1,
    key2: (value: TObject) => TKey2
): InternMap<TKey1, InternMap<TKey2, TObject[]>>;

export function group<TObject, TKey1, TKey2, TKey3>(
  iterable: Iterable<TObject>,
  key1: (value: TObject) => TKey1,
  key2: (value: TObject) => TKey2,
  key3: (value: TObject) => TKey3
): InternMap<TKey1, InternMap<TKey2, InternMap<TKey3, TObject[]>>>;

所以如下的使用就有问题:

// 超过了三个 Key 了:
const groups = group<Array, string, string, string, string>(data, ...);

其实不仅仅 group 有这种问题,比如 lodash 的 flow 方法也有同样的问题,只不过它的重载个数更多:

image.png

所以我觉得 group 函数也可以采用相同的思路:增加重载的个数,比如到9个?当不够的时候再使用新的数组语法。

DefinitelyTyped 讨论

为了解决心中的疑惑,我就直接去 DefinitelyTyped 对应的 PR 进行提问了。

这里简单介绍一下 DefinitelyTyped 这个库。这个库里是一些高质量的 TypeScript 的定义,为了让一些用 JavaScript 写的库能在 TypeScript 的项目里面有类型提示:比如 d3-array 和 loadsh 等。里面的类型定义主要是靠社区维护的,不一定是对应项目的开发者,比如 d3-array 就不是 d3 作者写的。

一般通过 $ npm i @types/xxx 进行安装,比如 $ npm i @types/d3-array

话说回来,我首先问了一下和 Breaking Change 相关的问题:

image.png

然后得到的答复如下:

image.png

大概的意思就是:“DefinitelyTyped 不完全遵循 Semantic Versioning,因为 types 避免 Breaking Changes 比 JS 困难。如果有需要的话,我可以提 PR 把之前的泛型通过重载的加回来。”

其实想想也觉得合理,TypeScript 提供的类型系统确实没有 JavaScript 本身稳定。虽然他提供了可以提 PR 把类型加回去的选项,但我觉得这个 Breaking Change 的影响也不大,长痛不如短痛,除非旧的接口更好。所以我问了我的第二个问题

image.png

最后得到的回答如下:

image.png

他指出了3个新 API 比旧 API 好的原因,我主要关注的还是第三个:就是 JS 和 type 的一致性,因为这个涉及到开发者使用和理解这 API 的心智。

简单来讲,就是他认为 group 的 keys 参数既可以被看作是数组也可以被看作是单独的参数,因为虽然传入的时候是单独的参数,但是函数内部消费的时候却是一个数组:

// 这里是单独的参数
function group(values, ...keys) {
  // 这里是数组
  return nest(values, identity, identity, keys);
}

怎么说呢,感觉也能接受?

然后后面就是我问了一个有点傻的问题,我以为 TS 支持以下的写法,所以就问他为啥不用这个语法,这样既可以解决问题,同时保持 API 统一,是因为兼容性的问题吗?

image.png

结果发现 TS 就不支持这种语法!!!是我 TS 学的不好.....

当然他也很 nice 地表示了没有关系

image.png

收获

经过这次讨论还是有很大的收获的:

  • @types 不完全遵循 Semantic Versioning,主要还是在影响面不大且比较重要的情况下。
  • 有问题就去社区问,只要你态度好,不管对错,大家还是很乐意解答的。
  • 一种替换重载定义 types 的方法。

最后如果希望帮助帮 G2 将 @types/d3-array 从 3.0.5 升级到 3.0.7,也欢迎提 PR!!!