类型推断的改进与 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

在类型中缓存 isGenericObjectTypeisGenericIndexType 这两个 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 属性类型中的 undefinednull,在 fooundefinednull 时整个表达式的值为 undefined。如果想要之前那样的效果,需要明确地加上括号。

} 和 > 现在不再是有效的 JSX 文本字符了

为了和 spec 保持一致,TypeScript 和 Babel 现在都禁止这两个字符出现在 JSX 文本中了,需要改用 HTML 转义符(例如 <div> 2 &gt; 1 </div>),或者插入字符串表达式(例如 <div> 2 {">"} 1 </div>),否则会报告错误信息,还有随之而来的编辑器的快速更正建议。

对交集类型(Intersections)和可选属性(Optional Properties)更严格的检查

在以前的版本中,对于 A & B 这样的交集类型,只要 AB 的其中一个可以赋值给 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