TypeScript 4.0
变长元组类型
考虑 JavaScript 中一个名为 concat 的函数,它接受两个数组或元组类型并将它们连接起来以创建一个新数组。
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}再考虑 tail,它接受一个数组或元组,并返回除第一个元素外的所有元素。
function tail(arg) {
const [_, ...result] = arg;
return result;
}我们如何在 TypeScript 中对它们进行类型化?
对于 concat,在旧版本的 TypeScript 中,我们唯一能做的就是尝试编写一些重载。
function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];呃...好吧,这只是当第二个数组为空时的七个重载。 让我们添加一些 arr2 有一个参数的情况。
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];我们希望这很清楚,这变得不合理了。 不幸的是,对于像 tail 这样的函数,你也会遇到同样的问题。
这是另一个我们称之为“千个重载之死”的情况,而且它甚至没有普遍解决问题。 它只为我们愿意编写的尽可能多的重载提供正确的类型。 如果我们想编写一个包罗万象的情况,我们需要一个类似下面的重载:
function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;但是当使用元组时,该签名没有编码输入的长度或元素的顺序。
TypeScript 4.0 带来了两个根本性的变化,以及推断改进,使得对这些进行类型化成为可能。
第一个变化是元组类型语法中的展开现在可以是泛型的。 这意味着即使我们不知道实际操作的类型,我们也可以表示对元组和数组的高阶操作。 当在这些元组类型中实例化(或替换为实际类型)泛型展开时,它们可以生成其他数组和元组类型的集合。
例如,这意味着我们可以对像 tail 这样的函数进行类型化,而不会出现“千个重载之死”的问题。
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
const r1 = tail(myTuple);
const r2 = tail([...myTuple, ...myArray] as const);
Try第二个变化是剩余元素可以出现在元组的任何位置——而不仅仅是在最后!
type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];以前,TypeScript 会发出如下错误:
A rest element must be last in a tuple type.但在 TypeScript 4.0 中,此限制已放宽。
请注意,当我们在类型中展开一个没有已知长度的类型时,结果类型也将变为无界,并且所有后续元素都会成为结果剩余元素类型的一部分。
type Strings = [string, string];
type Numbers = number[];
type Unbounded = [...Strings, ...Numbers, boolean];通过将这两种行为结合在一起,我们可以为 concat 编写一个单一的、类型良好的签名:
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}Try虽然那个签名仍然有点长,但它只是一个不需要重复的签名,并且对所有数组和元组都给出了可预测的行为。
这个功能本身就很棒,但在更复杂的场景中也大放异彩。 例如,考虑一个用于部分应用参数的函数,称为 partialCall。 partialCall 接受一个函数——我们称之为 f——以及 f 期望的前几个参数。 然后它返回一个新函数,该函数接受 f 仍然需要的任何其他参数,并在接收到它们时调用 f。
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs);
}TypeScript 4.0 改进了剩余参数和剩余元组元素的推断过程,以便我们可以对此进行类型化并使其“正常工作”。
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}Try在这种情况下,partialCall 理解它可以初始接受哪些参数以及不能接受哪些参数,并返回适当地接受和拒绝任何剩余参数的函数。
const foo = (x: string, y: number, z: boolean) => {};
const f1 = partialCall(foo, 100);
const f2 = partialCall(foo, "hello", 100, true, "oops");
// 这有效!
const f3 = partialCall(foo, "hello");
// 现在我们能用 f3 做什么?
// 有效!
f3(123, true);
f3();
f3(123, "hello");Try变长元组类型开启了许多令人兴奋的新模式,尤其是在函数组合方面。 我们期望我们可能能够利用它来更好地对 JavaScript 的内置 bind 方法进行类型检查。 还有少数其他推断改进和模式也加入其中,如果你有兴趣了解更多,可以查看变长元组的拉取请求。
带标签的元组元素
改善元组类型和参数列表的体验很重要,因为它允许我们对常见的 JavaScript 习语进行强类型验证——实际上只是对参数列表进行切片和切块并传递给其他函数。 我们可以将元组类型用于剩余参数的想法是至关重要的地方之一。
例如,以下使用元组类型作为剩余参数的函数……
function foo(...args: [string, number]): void {
// ...
}……对于 foo 的任何调用者来说,应该与以下函数没有区别……
function foo(arg0: string, arg1: number): void {
// ...
}然而,有一个地方开始出现差异:可读性。 在第一个例子中,我们没有第一个和第二个元素的参数名称。 虽然这些对类型检查没有影响,但元组位置上缺少标签会使它们更难使用——更难传达我们的意图。
这就是为什么在 TypeScript 4.0 中,元组类型现在可以提供标签。
type Range = [start: number, end: number];为了加深参数列表和元组类型之间的联系,剩余元素和可选元素的语法镜像了参数列表的语法。
type Foo = [first: number, second?: string, ...rest: any[]];使用带标签的元组时有几条规则。 首先,当标记一个元组元素时,元组中的所有其他元素也必须被标记。
值得注意的是——标签并不要求我们在解构时以不同方式命名变量。 它们纯粹用于文档和工具。
function foo(x: [first: string, second: number]) {
// ...
// 注意:我们不需要将这些命名为 'first' 和 'second'
const [a, b] = x;
a
b
}Try总的来说,当利用元组和参数列表的模式以及以类型安全的方式实现重载时,带标签的元组非常方便。 实际上,TypeScript 的编辑器支持将尽可能将它们显示为重载。

要了解更多信息,请查看带标签元组元素的拉取请求。
从构造函数推断类属性
TypeScript 4.0 现在可以在启用 noImplicitAny 时使用控制流分析来确定类中属性的类型。
class Square {
// 以前这两个都是 any
area;
sideLength;
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}Try在构造函数的某些路径未分配给实例成员的情况下,该属性被认为可能是 undefined。
class Square {
sideLength;
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2; }
}Try在你知道更好的情况下(例如,你有某种 initialize 方法),如果你处于 strictPropertyInitialization 下,你仍然需要一个显式类型注解以及一个明确赋值断言(!)。
class Square {
// 明确赋值断言
// v
sideLength!: number;
// 类型注解
constructor(sideLength: number) {
this.initialize(sideLength);
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}Try有关更多详细信息,请参阅实现拉取请求。
短路赋值运算符
JavaScript 和许多其他语言支持一组称为复合赋值运算符的运算符。 复合赋值运算符将运算符应用于两个参数,然后将结果赋值给左侧。 你可能以前见过这些:
// 加法
// a = a + b
a += b;
// 减法
// a = a - b
a -= b;
// 乘法
// a = a * b
a *= b;
// 除法
// a = a / b
a /= b;
// 幂运算
// a = a ** b
a **= b;
// 左位移
// a = a << b
a <<= b;JavaScript 中有这么多运算符都有对应的赋值运算符! 然而,直到最近,还有三个值得注意的例外:逻辑与(&&)、逻辑或(||)和空值合并(??)。
这就是为什么 TypeScript 4.0 支持一个新的 ECMAScript 特性,添加三个新的赋值运算符:&&=、||= 和 ??=。
这些运算符非常适合替代用户可能编写如下代码的任何示例:
a = a && b;
a = a || b;
a = a ?? b;或者类似的 if 块,如
// 可以写成 'a ||= b'
if (!a) {
a = b;
}甚至还有一些我们见过的模式(或者,呃,我们自己写的)来惰性初始化值,只在需要时才初始化。
let values: string[];
(values ?? (values = [])).push("hello");
// 之后
(values ??= []).push("hello");(看,我们并不为我们编写的所有代码感到自豪……)
在你使用带有副作用的 getter 或 setter 的罕见情况下,值得注意的是,这些运算符仅在必要时执行赋值。 从这个意义上说,不仅是运算符的右侧“短路”——赋值本身也是。
obj.prop ||= foo();
// 大致等价于以下任一
obj.prop || (obj.prop = foo());
if (!obj.prop) {
obj.prop = foo();
}尝试运行以下示例 看看这与总是执行赋值有何不同。
const obj = {
get prop() {
console.log("getter has run");
// 替换我!
return Math.random() < 0.5;
},
set prop(_val: boolean) {
console.log("setter has run");
}
};
function foo() {
console.log("right side evaluated");
return true;
}
console.log("This one always runs the setter");
obj.prop = obj.prop || foo();
console.log("This one *sometimes* runs the setter");
obj.prop ||= foo();Try我们非常感谢社区成员 Wenlu Wang 对此的贡献!
有关更多详细信息,你可以在此处查看拉取请求。 你还可以查看 TC39 关于此功能的提案仓库。
catch 子句绑定中的 unknown
从 TypeScript 诞生之初,catch 子句变量就一直被类型化为 any。 这意味着 TypeScript 允许你对它们做任何想做的事。
try {
// 做一些工作
} catch (x) {
// x 的类型为 'any' - 尽情享受吧!
console.log(x.message);
console.log(x.toUpperCase());
x++;
x.yadda.yadda.yadda();
}Try如果我们试图防止错误处理代码中发生更多错误,上述行为有些不可取! 因为这些变量默认具有类型 any,它们缺乏任何类型安全性,这可能会在无效操作上出错。
这就是为什么 TypeScript 4.0 现在允许你将 catch 子句变量的类型指定为 unknown。 unknown 比 any 更安全,因为它提醒我们,在对值进行操作之前,需要执行某种类型检查。
try {
// ...
} catch (e: unknown) {
// 不能访问 unknown 上的值
console.log(e.toUpperCase());
if (typeof e === "string") {
// 我们将 'e' 收窄为类型 'string'。
console.log(e.toUpperCase());
}
}Try虽然 catch 变量的类型默认不会改变,但我们可能会考虑在将来添加一个新的 strict 模式标志,以便用户可以选择加入此行为。 同时,应该可以编写一个 lint 规则来强制 catch 变量具有 : any 或 : unknown 的显式注解。
有关更多详细信息,你可以查看此功能的更改。
自定义 JSX 工厂
当使用 JSX 时,片段是一种允许我们返回多个子元素的 JSX 元素类型。 当我们最初在 TypeScript 中实现片段时,我们并不清楚其他库将如何利用它们。 如今,大多数鼓励使用 JSX 并支持片段的其他库都具有类似的 API 形状。
在 TypeScript 4.0 中,用户可以通过新的 jsxFragmentFactory 选项自定义片段工厂。
例如,以下 tsconfig.json 文件告诉 TypeScript 以与 React 兼容的方式转换 JSX,但将每个工厂调用切换为 h 而不是 React.createElement,并使用 Fragment 而不是 React.Fragment。
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}在需要为每个文件使用不同 JSX 工厂的情况下,你可以利用新的 /** @jsxFrag */ 编译指示注释。 例如,以下代码……
// 注意:这些编译指示注释需要使用 JSDoc 风格的多行语法编写才能生效。
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
export const Header = (
<>
<h1>Welcome</h1>
</>
);Try……将被转换为这个输出的 JavaScript……
"use strict";
// 注意:这些编译指示注释需要使用 JSDoc 风格的多行语法编写才能生效。
Object.defineProperty(exports, "__esModule", { value: true });
exports.Header = void 0;
/** @jsx h */
/** @jsxFrag Fragment */
const preact_1 = require("preact");
exports.Header = ((0, preact_1.h)(preact_1.Fragment, null,
(0, preact_1.h)("h1", null, "Welcome")));
Try我们非常感谢社区成员 Noj Vek 发送了这个拉取请求并耐心地与我们的团队合作。
你可以在拉取请求中查看更多详细信息!
在 build 模式下使用 --noEmitOnError 的速度改进
以前,在 incremental 下,在先前编译有错误之后编译程序,当使用 noEmitOnError 标志时会非常慢。 这是因为上一次编译的信息都不会基于 noEmitOnError 标志缓存在 .tsbuildinfo 文件中。
TypeScript 4.0 改变了这一点,在这些场景中带来了巨大的速度提升,并进而改进了 --build 模式场景(这隐含了 incremental 和 noEmitOnError)。
有关详细信息,请阅读更多关于拉取请求的内容。
--incremental 与 --noEmit
TypeScript 4.0 允许我们在利用 incremental 编译的同时使用 noEmit 标志。 以前这是不允许的,因为 incremental 需要输出 .tsbuildinfo 文件;然而,启用更快的增量构建的用例足够重要,可以面向所有用户启用。
有关更多详细信息,你可以查看实现拉取请求。
编辑器改进
TypeScript 编译器不仅在大多数主要编辑器中为 TypeScript 本身提供编辑体验——它还在 Visual Studio 系列编辑器及其他编辑器中为 JavaScript 体验提供支持。 因此,我们的大部分工作都集中在改进编辑器场景——作为开发人员,你大部分时间都在这里度过。
在编辑器中使用新的 TypeScript/JavaScript 功能将根据你的编辑器而有所不同,但是
- Visual Studio Code 支持选择不同版本的 TypeScript。或者,可以使用 JavaScript/TypeScript 夜间扩展来保持最新状态(通常非常稳定)。
- Visual Studio 2017/2019 有[上述 SDK 安装程序]和 MSBuild 安装。
- Sublime Text 3 支持选择不同版本的 TypeScript
你可以查看支持 TypeScript 的编辑器部分列表以了解你喜欢的编辑器是否支持使用新版本。
转换为可选链
可选链是一个最近的功能,受到了很多喜爱。 这就是为什么 TypeScript 4.0 带来了一个新的重构,将常见模式转换为利用可选链和空值合并!

请记住,由于 JavaScript 中真值/假值的微妙之处,虽然此重构并不完全捕获相同的行为,但我们相信它应该捕获大多数用例的意图,特别是当 TypeScript 对你的类型有更精确的了解时。
有关更多详细信息,请查看此功能的拉取请求。
/** @deprecated */ 支持
TypeScript 的编辑支持现在识别声明是否已被标记为 /** @deprecated */ JSDoc 注释。 该信息显示在补全列表中,并作为建议诊断,编辑器可以特殊处理。 在像 VS Code 这样的编辑器中,已弃用的值通常以删除线样式显示 像这样。

这个新功能要归功于 Wenlu Wang。 有关更多详细信息,请参阅拉取请求。
启动时的部分语义模式
我们听到了很多用户对启动时间过长的抱怨,尤其是在较大的项目上。 罪魁祸首通常是一个称为程序构建的过程。 这是从一组初始根文件开始,解析它们,找到它们的依赖项,解析这些依赖项,找到这些依赖项的依赖项,依此类推的过程。 你的项目越大,在获得诸如跳转到定义或快速信息等基本编辑器操作之前,你需要等待的时间就越长。
这就是为什么我们一直在为编辑器开发一种新模式,以在完整语言服务体验加载之前提供部分体验。 其核心思想是,编辑器可以运行一个轻量级的部分服务器,只查看编辑器当前打开的文件。
很难确切地说你会看到什么样的改进,但根据经验,在 Visual Studio Code 代码库上,TypeScript 变得完全响应之前,通常需要20 秒到一分钟的时间。 相比之下,我们的新部分语义模式似乎将延迟降低到了仅仅几秒钟。 例如,在下面的视频中,你可以看到两个并排的编辑器,左边运行 TypeScript 3.9,右边运行 TypeScript 4.0。
当在特别大的代码库上重新启动两个编辑器时,TypeScript 3.9 的那个根本无法提供补全或快速信息。 另一方面,带有 TypeScript 4.0 的编辑器可以立即在我们正在编辑的当前文件中提供丰富的体验,同时在后台加载完整项目。
目前唯一支持此模式的编辑器是 Visual Studio Code,它将在 Visual Studio Code Insiders 中带来一些 UX 改进。 我们认识到这种体验在 UX 和功能方面可能仍有改进空间,并且我们有一系列改进的想法。 我们正在寻求更多关于你认为可能有用的反馈。
有关更多信息,你可以查看原始提案、实现拉取请求以及后续的元问题。
更智能的自动导入
自动导入是一个很棒的功能,它使编码变得容易得多;然而,每当自动导入似乎不起作用时,它可能会让用户感到困惑。 我们听到用户的一个具体问题是,自动导入不适用于用 TypeScript 编写的依赖项——也就是说,直到他们在项目中的其他地方至少编写了一个显式导入。
为什么自动导入对 @types 包有效,但对提供自己类型的包无效? 事实证明,自动导入仅对项目已经包含的包有效。 因为 TypeScript 有一些古怪的默认设置,会自动将 node_modules/@types 中的包添加到你的项目中,那些包会被自动导入。 另一方面,其他包被排除在外,因为爬取所有 node_modules 包可能非常昂贵。
所有这些都导致当你尝试自动导入刚刚安装但尚未使用的东西时,入门体验相当糟糕。
TypeScript 4.0 现在在编辑器场景中做了一些额外的工作,以包含你在 package.json 的 dependencies(和 peerDependencies)字段中列出的包。 这些包中的信息仅用于改进自动导入,不会改变其他任何东西,如类型检查。 这使我们能够为所有具有类型的依赖项提供自动导入,而无需承担完整的 node_modules 搜索的成本。
在极少数情况下,如果你的 package.json 列出了超过十个尚未导入的类型化依赖项,此功能会自动禁用,以防止项目加载缓慢。 要强制该功能工作,或完全禁用它,你应该能够配置你的编辑器。 对于 Visual Studio Code,这是“Include Package JSON Auto Imports”(或 typescript.preferences.includePackageJsonAutoImports)设置。
我们的新网站!
TypeScript 网站最近已经从头开始重写并推出!

我们已经写了一篇关于我们新网站的文章,所以你可以在那里阅读更多;但值得一提的是,我们仍在期待听到你的想法! 如果你有问题、评论或建议,你可以在网站的议题跟踪器上提交。
破坏性更改
lib.d.ts 更改
我们的 lib.d.ts 声明已更改——最具体地说,DOM 的类型已更改。 最值得注意的变化可能是删除了 document.origin,它仅适用于旧版本的 IE 和 Safari。 MDN 建议迁移到 self.origin。
属性覆盖访问器(反之亦然)是错误
以前,仅在启用 useDefineForClassFields 时,属性覆盖访问器或访问器覆盖属性才被视为错误;然而,TypeScript 现在在派生类中声明将覆盖基类中的 getter 或 setter 的属性时总是会发出错误。
class Base {
get foo() {
return 100;
}
set foo(value) {
// ...
}
}
class Derived extends Base {
foo = 10;}Try查看实现拉取请求上的更多详细信息。
delete 的操作数必须是可选的。
在 strictNullChecks 下使用 delete 运算符时,操作数现在必须是 any、unknown、never,或者是可选的(即类型中包含 undefined)。否则,使用 delete 运算符是错误的。
查看实现拉取请求上的更多详细信息。
使用 TypeScript 的节点工厂已弃用
今天,TypeScript 提供了一组用于生成 AST 节点的“工厂”函数;然而,TypeScript 4.0 提供了一个新的节点工厂 API。 因此,对于 TypeScript 4.0,我们决定弃用这些旧函数,转而使用新函数。
有关更多详细信息,请阅读此更改的相关拉取请求。
有关更多详细信息,你可以查看