升级TS,听起来应该是件很简单的事,如果你的项目相对独立那确实稍微花点时间就做完了。但通常一个大型项目会将各个模块分给每个小的业务组,还有负责基础库的基架组,这时候想要单独升级业务模块的TS版本,就要考虑对上游和下游的影响了。
麻烦的问题就来了,TS并不遵守 Semantic Version
,它除了bugfix以外的版本更新几乎都会带来Break Changes
。这里的Break Changes
可能是两个方面的:
- 低版本TS代码在高版本TS下编译会报错,也就是说升级业务模块时要考虑底层依赖的影响。
- 高版本TS编译出来的
d.ts
类型定义在低版本TS下编译会报错,这时就得考虑怎么样升级TS后模块的编译生成的文件给上层应用使用时不会报错。
这篇文章会对上面两个问题给出我的解决方案,另外由于这篇文章是我去年年底写的,所以只考虑到TS3.0~3.7
遇到的问题。
另外本文只提到了二方包情况,对于社区维护三方包通常会跟随TS版本升级和做兼容的,出现问题先升一下版本。
低版本TS代码在高版本TS下的错误
这个问题很好解决,只要让代码在两个版本的TS下同时编译通过即可。当然这要求你去修复所有报错的依赖库,我建议有权限的话就自己做,因为这里的错误不会太多相比跨组协同的成本不值一提。
下面我总结了一些我遇到的问题以及解决方法。
可能会遇到的问题
error TS2300: Duplicate identifier ‘IteratorResult’.
升级@types/node
即可。
范型不再默认为{}
类型,改为了unknown
在3.5以下的版本中范型变量的类型默认为{}
,在3.5之后你必须显式地指定范型上界T extends {}
或者通过type guard才能使参数为对象类型。see detail
// error
function foo<T>(a: T) {
return a.toString();
}
// pass
function foo<T extends {}>(a: T) {
return a.toString();
}
// pass: use type guard
function isObject(data: any): data is object {
return typeof data === 'object' && data != null;
}
function foo<T>(a: T) {
if (isObject(a)) {
return a.toString();
}
}
Property ‘wheelDelta’ does not exist on type ‘WheelEvent’.
无法推断的类型从any变为unknown
之前在new Set()
如果没有指定范型,则会默认给any
,现在改为了unknown
。在自定义的范型函数中也很容易出现这个问题,原来infer不出来的类型会给个any
,现在都改为了unknown
。
// set is Set<unknown> now
const set = new Set();
// error
set.add(1);
const set = new Set<number>();
// pass
set.add(1);
TS2744: Type parameter defaults can only reference previously declared type parameters.
在ts3.3之后在范型的默认值里就只能引用该项之前的类型。
// error
class A<T = D, D = string> {}
// work
class A<D = string, T = D> {}
Property ‘children’ does not exist on type ‘xxx’
出现这个错误很可能这个React组件使用里函数的方式来声明(如下)
function MyCom(props: { href: string }) {
return <a {...props} />;
}
这样子声明的组件是不会给Props自动加上{ children?: ReactNode }
的。在旧版本的ts里对children的检查不会很严格所以不会报错,但这个问题在新版本里就会显现。
解决方法:
- 使用
React.FC
或React.SFC
来指定类型 - 手动加上
children
类型。
在3.3之后类继承时方法会应用逆变逻辑
比如下面的代码在3.3以上时会报错
interface A {
a: string;
}
interface B extends A {
b: number;
}
class CA {
static a: (a: A) => void;
}
class CB extends CA {
// 由于函数参数是逆变的这里会导致报错
static a: (a: B) => void;
}
一般来说只要类型没写错是不会遇到什么问题,但是我们项目中使用了React组件继承的写法,加上contextType
时就会暴露问题了。它的类型React.Context
是个逆变的类型,但是context
又是协变的,这样就很矛盾了。所以不建议继承组件的Context
,当然组件继承和Context
继承都没啥意义。要写还是得hack一下类型。
// class B extends A {}
// a = new A()
// b = new B()
class A extends React.Component<any> {
// contextType交给react使用的,不会被我们用到,所以这里指定类型为any
static contextType: React.Context<any> = React.createContext(a);
context: A;
}
class B extends A {
static contextType = React.createContext(b);
context: B;
}
高版本TS编译出来的d.ts
类型定义在低版本TS下的报错
这部分错误不太多,就一个个来解决
一些错误
An accessor cannot be declared in an ambient context.ts(1086)
ts@3.7会在.d.ts
里生成 getter
和 setter
,ts@<3.6会不能识别。
Cannot find name ‘readonly’
3.4后会默认将函数参数类型的Readonly转为readonly关键字
TS2315: Type ‘Generator’ is not generic
3.6修改了Generator类型,导致编译generator函数生成的类型在旧版本TS里会报错。
解决方法
为了避免引用方大规模更新ts,需要考虑兼容方案。上面第二个问题可以通过指定类型来解决,第三个问题也一样,不过考虑到3.6以下Generator
不能指定元素类型,可以使用IterableIterator
作为返回类型。
第一个问题的话目前没有兼容性方案。不过在ts@3.1里支持了根据ts版本来选择文件的功能,这样我们只要通过这个工具,就能打包出一个兼容低版本的.d.ts
文件。(PS: downlevel-dts只支持兼容到3.4版本,3.4下对很常用的readonly array&tuple是不支持的,我在自己的fork里加了这个支持,代码见PR)。
跨组升级
虽然写了很多兼容方法,但我在实际执行时还是向着推动整个组的项目升级TS做的,第二个问题其实可以不用考虑,所以我也不能保证像 downlevel-dts 不会有什么其他问题。我其实也非常建议推动整个组升级,虽然跨组沟通成本很高,但是能将收益与风险向着好的方向走。
这里我简要讲一下如何跨组推动升级TS,这里很重要的一点是要换位思考,可以想想当别的组给你们提了一个需求的时候,你希望了解什么?这样我们就能自己归纳出需要做的准备了,因为每个公司流程不太一样,我就拿自己的做法来举例。
我首先思考的是就是经典的三大问题,这个需求是做啥?为什么要做?怎么做?第一个问题对于前端来说不需要解释,关于第二个问题的话,我建议将TS升级后更严格的类型检查作为主要优势来讲,这些可以在升级自己项目时或者尝试升级其他组项目时,把遇到的问题记录下来作为例子(问题数和升级成本是成反比的,如果实在没啥可以举例的,那么直接说升级成本很低就行了)。新语法新特性附上链接即可。怎么做的话我上面已经讲了很多了,你可以根据自己公司的情况来添加修改。
回到推进方来思考,我们面临的第一个问题是升级的范围和顺序。如果不能一次性全升完并且项目之间有依赖的话,由于之前提到的不兼容,就得考虑项目的升级顺序。我们公司的项目分为APP、插件和二方包3层,从左往右升就ok了。这里各个业务方主要开发的是插件,所以我的顺序是:
修复二方包里TS在新版本里的问题 -> 通知所有业务方修复TS问题或者直接升级TS(因为APP里基本没有代码所以一般可以不考虑兼容) -> 升级APP的TS版本 -> 通知业务方升级TS -> 升级二方包的TS。
这里插件间也会有依赖还比较复杂,如果要一级级升会比较耗时。恰逢底层框架的升级,我就先挑了依赖该框架的插件来一起升级,这里我用到了yarn list --json
来生成依赖树并找到所有的依赖。
上面这些计划一定要写好时间表,并给deadline预留一个月左右的时间,这样能减少因为某些组延期导致后续也要推迟的概率。