类型推断的改进与 Promise.all
这里指的是对“元组到元组”的类型推断的改进(个人觉得这应该算 bug 修复,这个 bug 是 Anders 老爷亲自写出来的……)。
在过去的版本,下面的代码会导致错误,为 str
推断出的类型并不是对应位置的 "hello"
,而是所有元素类型的 union:
const [str, _] = await Promise.all(['hello', undefined]);
str.toString(); // error TS2532: Object is possibly 'undefined'.
导致这个问题的具体原因是,“数组/元组”类型是“对象”类型的一种,“对象”类型可以有“属性”、“调用签名”、“构造签名”,以及“索引签名”,在推断类型时依次进行推断。在过去,对“数组/元组”的类型推断是在“属性类型推断”这一过程中进行的,然而在后续对“索引类型推断”中覆盖了“属性类型推断”的结果,于是上述情景下便得到了“索引类型推断”的结果,也就是所有元素类型的 union。
现在 3.9 修复了这个问题,通过将“数组/元组到元组的推断”这种情况作为单独的过程提取出来。
原定的 awaited 类型被延后支持
这是一个用途非常单一的新特性,暂时延后加入了。
编译的速度提升了!
PR 36576:
在过去的版本中,对泛型形参到实参类型的映射关系是通过闭包来保存的,每个映射要创建一个函数对象,调用它得到映射结果,现在改为了用对象保存,避免创建函数对象,这个改动降低了约 8% 的内存消耗,并提升了 1% 到 9%(在老版本的 Node.js 上)的性能。
PR 36590:
对于复杂的类型,TypeScript通过缓存类型检查的结果来提高性能,3.9 优化了逻辑判断的路径,将一些开销低的判断提前。这个改动在测试中减少了约 6% 的编译时间。
PR 36607:
“类型引用”是一种语法节点,表示对“泛型类”或“泛型接口”的引用(包含了引用的符号和参数),在过去的版本中,如果“类型引用”是在“类型别名”(type
语法)的定义中立即地引用(需要立刻解出类型),那么在解该“类型引用(节点)”的类型时将为它创建“延迟类型引用(类型)”。这一设计是为了支持类型递归。
在 3.9 中,上述规则被细化为:如果“类型引用”被“类型别名”直接定义为别名(包括仅有括号或 readonly
修饰的情况),或是包含于可能存在循环引用的“类型别名”定义中,那么为它创建“延迟类型引用”。这个改动在测试中减少了约 4% 的编译时间。
PR 36622
在类型中缓存 isGenericObjectType
和 isGenericIndexType
这两个 API 的结果。这两个 API 是用来辨别类型是否是可实例化的(泛型参数、泛型参数的映射类型、泛型参数的索引类型),因为对这些类型的“条件类型”、“索引访问类型”和“索引类型”需要延迟解决。因为传入的类型可能是元素很多的联合类型(union)或集合类型(intersection),所以增加了两个标志在类型中缓存这两个 API 的结果。 这个改动在测试中减少了约 6.5% 的编译时间。
PR 36754
改写了对“映射类型”的属性类型是否包含 undefined
类型的判断,这一部分后来又进一步改写了。总之,这个改动在测试中减少了约 8% 到 10% 的编译时间,顺便解决了一个循环引用的 bug。
PR 36696
多个对象类型如果具有不相交类型的判别属性,它们的集合类型(intersection)被简化为 never
,而在以前的版本中,结果是一个其判别属性为 never
类型的对象。这个改动在测试中略微减少了编译时间。
PR 37055
优化了模块路径解析缓存,改善了编辑器中重命名文件时自动更新 import 语句的速度。
新增 @ts-expect-error 注释
这个注释用来指示下一行代码预期应该包含错误,下一行代码的错误将不会被编译器报告。与 @ts-ignore
不同的是,如果下一行代码没有错误,该注释将产生一个错误(TS2578: Unused '@ts-expect-error' directive.)。这在对类型进行预期失败的测试时很有用。
条件表达式中未调用函数的检查
在 if
语句的条件中如果仅写了函数名而忘记写括号调用,编译器会报告错误。现在条件表达式 ?:
也支持这个特性了。
改进编辑器支持
改进了JavaScript 里 CommonJS 模块中的自动导入
在 JavaScript 中编写 CommonJS 模块的代码时,TypeScript 现在会自动检测正在使用的导入语句类型是 ECMAScript 风格还是 CommonJS 风格,以保持源文件风格整洁统一。
代码操作保留换行
现在在编辑器中重构代码时可以保留原代码中的空行了,例如将代码提取为函数时。
快速更正缺失返回语句
当为箭头函数加上大括号时,可以用快速更正功能自动在表达式前添加 return 关键字。
支持解决方案式(solution style)tsconfig.json 文件
通常情况下,由 TypeScript 语言服务支持的编辑器通过逐级目录向上查找 "tsconfig.json" 来确定一个文件属于哪个配置文件。如果想要编辑器正确地关联源文件和配置文件,那么配置文件必须命名为 "tsconfig.json",因此一个目录下也不能存在两个配置文件(只有名为 "tsconfig.json" 的那个才能被识别到)。
现在引入了一种新的配置文件,示例如下:
// tsconfig.json
{
"files": [],
"references": [
{ "path": "./tsconfig.shared.json" },
{ "path": "./tsconfig.frontend.json" },
{ "path": "./tsconfig.backend.json" },
]
}
这种实际上什么都不做的配置文件就类似于 Visual Studio 的解决方案,它引用了多个项目。语言服务可以通过这个文件,查找到源文件属于其中哪一个引用的配置文件。顺便一提,这个解决方案式配置文件仍然需要命名为 tsconfig.json,并且在源文件的同目录或父目录,但是它引用的多个配置文件可以随意放置在任意目录了。
一些破坏性更新
可选链(Optional Chaining)和非空断言(Non-Null Assertions)的解析规则变化
在之前的实现中,以下代码:
foo?.bar!.baz
被解释为等价于以下代码:
(foo?.bar).baz
也就是说,非空断言针对的是左边整个表达式部分 foo?.bar
,而不是单独的 bar
属性。这样的行为对于解析器来说简单,但对用户来说反直觉,进而容易导致类型不安全(想象一下左边有多个 ?.
,而有人不明白 !
的作用范围)。
现在上述代码中的非空断言将只去除表达式中 bar
属性类型中的 undefined
和 null
,在 foo
为 undefined
或 null
时整个表达式的值为 undefined
。如果想要之前那样的效果,需要明确地加上括号。
} 和 > 现在不再是有效的 JSX 文本字符了
为了和 spec 保持一致,TypeScript 和 Babel 现在都禁止这两个字符出现在 JSX 文本中了,需要改用 HTML 转义符(例如 <div> 2 > 1 </div>
),或者插入字符串表达式(例如 <div> 2 {">"} 1 </div>
),否则会报告错误信息,还有随之而来的编辑器的快速更正建议。
对交集类型(Intersections)和可选属性(Optional Properties)更严格的检查
在以前的版本中,对于 A & B
这样的交集类型,只要 A
和 B
的其中一个可以赋值给 C
,整个交集类型就可以赋值给 C
。然而有时遇到可选属性时会有问题,例如:
interface A {
a: number; // notice this is 'number'
}
interface B {
b: string;
}
interface C {
a?: boolean; // notice this is 'boolean'
b: string;
}
declare let x: A & B;
declare let y: C;
y = x;
由于 B
类型可以赋值给 C
类型,所以 A & B
类型被错误地判定为可以赋值给 C
类型,但两者的 a
属性类型并不兼容。
现在在 TypeScript 3.9 中, 类型系统会检查交集类型中每个属性是否都兼容目标类型,结果就是上述代码会报告类型错误。
这次发布的原文说,交集类型中只有具体对象类型才适用(so long as every type in an intersection is a concrete object type),不过我跑去看了一下相关的源码发现已经改了,实际上没有这个限制了。
判别属性(Discriminant Properties)对交集类型(Intersections)的精简
这个在上面性能优化里 PR 36696 已经提到了,这是一个破坏性更新。
Getters/Setters 不再是可遍历的
在以前的版本中,转译目标为 ES5 时,类的 get/set 访问器是定义为可遍历属性的,然而这不符合标准,所以现在 TypeScript 3.9 中改为不可遍历属性了。
约束为 any 的泛型参数表现不再等同于 any
引用官方给出的例子说明:
function foo<T extends any>(arg: T) {
arg.spfjgerijghoied; // no error!
}
在以前的版本中约束为 any
的泛型参数表现得就像 any
一样,TypeScript 3.9 中采用了更保守的方案,上述代码会报错。
总是保留 export *
语句
在以前的版本中,在 foo
模块没有导出任何值的情况下,export * from "foo"
不会输出到编译结果中。这个改动是为了照顾 Babel,因为之前的行为是由类型导向的,所以 Babel 不太可能效仿这个行为。这个改动应该不会破坏太多现有代码,但 bundler 工具们在这种情况下要 tree-shaking 掉代码需要多费点劲了。(动态类型一时爽……)
从模块导出符号现在通过 Getters 实时绑定啦
编译模块到 ES5 及以上的 CommonJS 这样的模块系统时,TypeScript 将使用 get 访问器来模拟实时绑定,这里指的是 export * from "foo"
这样的导出,这样原模块中导出变量的值改变就对导出模块可见了。这个改变是为了更贴近标准。
导出的符号现在会被提升并初始化值
编译模块到 ES5 及以上的 CommonJS 这样的模块系统时,TypeScript 现在会提升导出的声明到文件顶部,并初始化为 undefined
。这个是对上面的改变的兼容,由于 export * from
会在 exports
上定义 getter,如果当前模块在之后有重名的导出符号,会因为不能写属性而失败,所以这里提前将当前模块的导出符号写到 exports
上,在 export * from
的时候跳过已经有的属性。不过试了一下发现没有处理 export * as
的情况,在 PR 下面反馈了。
以上就是 TypeScript 3.9 的更新内容。
参考链接: [1] https://devblogs.microsoft.com/typescript/announcing-typescript-3-9