可变元组类型(Variadic Tuple Types)
设想一个名为 concat 的 JavaScript 函数,它接收两个数组或元组类型,并将它们连接在一起以创建一个新数组。
jsfunction concat(arr1, arr2) {return [...arr1, ...arr2];}
再考虑 tail 函数,它接收一个数组或元组,并返回除第一个元素外的所有元素。
jsfunction tail(arg) {const [_, ...result] = arg;return result;}
我们该如何在 TypeScript 中为这两个函数编写类型声明?
对于 concat,在旧版本的语言中,唯一可行的方法是编写多个重载。
tsfunction 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 包含一个参数的情况添加一些。
tsfunction 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 这样的函数编写类型时,你最终也会遇到同样的问题。
这是我们称之为“千次重载导致的死亡”(death by a thousand overloads)的另一个案例,而且它甚至不能从根本上解决问题。它只能为你所写的重载数量提供正确的类型。如果我们想创建一个万能的包罗万象的情况,我们需要像下面这样的重载:
tsfunction concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;
但是,当使用元组时,该签名并没有编码任何关于输入长度或元素顺序的信息。
TypeScript 4.0 带来了两个根本性的变化,以及一些推断改进,使得实现这些类型成为可能。
第一个变化是元组类型语法中的展开(spread)现在可以是泛型的。这意味着即使在我们不知道所操作的具体类型时,也可以表示元组和数组的高阶操作。当这些泛型展开在元组类型中被实例化(或被替换为真实类型)时,它们可以产生其他数组和元组类型集合。
例如,这意味着我们可以为 tail 这样的函数编写类型,而无需陷入“千次重载”的困境。
tsTryfunctiontail <T extends any[]>(arr : readonly [any, ...T ]) {const [_ignored , ...rest ] =arr ;returnrest ;}constmyTuple = [1, 2, 3, 4] asconst ;constmyArray = ["hello", "world"];constr1 =tail (myTuple );constr2 =tail ([...myTuple , ...myArray ] asconst );
第二个变化是剩余元素(rest elements)可以出现在元组的任何位置——而不仅仅是在末尾!
tstype 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 中,这个限制被放宽了。
请注意,在我们展开一个长度未知的类型的情况下,结果类型也将变为非受限(unbounded),并且所有后续元素都会被计入结果的剩余元素类型中。
tstype Strings = [string, string];type Numbers = number[];type Unbounded = [...Strings, ...Numbers, boolean];
通过结合这两种行为,我们可以为 concat 编写一个类型良好的单一签名:
tsTrytypeArr = readonly any[];functionconcat <T extendsArr ,U extendsArr >(arr1 :T ,arr2 :U ): [...T , ...U ] {return [...arr1 , ...arr2 ];}
虽然那个签名仍然有点长,但它只需要写一次,不需要重复,并且在所有数组和元组上都能提供可预测的行为。
这项功能本身就很棒,但在更复杂的场景中更是大放异彩。例如,考虑一个名为 partialCall 的偏函数应用函数。partialCall 接收一个函数——我们称之为 f——以及 f 所期望的初始几个参数。然后,它返回一个新的函数,该函数接收 f 仍然需要的任何其他参数,并在接收到它们时调用 f。
jsfunction partialCall(f, ...headArgs) {return (...tailArgs) => f(...headArgs, ...tailArgs);}
TypeScript 4.0 改进了剩余参数和剩余元组元素的推断过程,以便我们能够为它编写类型,并让它“自然而然地工作”。
tsTrytypeArr = readonly unknown[];functionpartialCall <T extendsArr ,U extendsArr ,R >(f : (...args : [...T , ...U ]) =>R ,...headArgs :T ) {return (...tailArgs :U ) =>f (...headArgs , ...tailArgs );}
在这种情况下,partialCall 能够理解它最初可以或不可以接收哪些参数,并返回适当接收或拒绝剩余内容的函数。
tsTryconstfoo = (x : string,y : number,z : boolean) => {};constArgument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.f1 =partialCall (foo ,100 );constExpected 4 arguments, but got 5.2554Expected 4 arguments, but got 5.f2 =partialCall (foo , "hello", 100, true,"oops" );// This works!constf3 =partialCall (foo , "hello");// What can we do with f3 now?// Works!f3 (123, true);Expected 2 arguments, but got 0.2554Expected 2 arguments, but got 0.(); f3 Argument of type 'string' is not assignable to parameter of type 'boolean'.2345Argument of type 'string' is not assignable to parameter of type 'boolean'.f3 (123,"hello" );
可变元组类型开启了许多令人兴奋的新模式,尤其是在函数组合方面。我们期望能够利用它来更好地对 JavaScript 内置的 bind 方法进行类型检查。为此,还加入了一些其他的推断改进和模式,如果你有兴趣了解更多,可以查看关于可变元组的拉取请求。
带标签的元组元素(Labeled Tuple Elements)
改善围绕元组类型和参数列表的体验非常重要,因为它允许我们就常见的 JavaScript 惯用语(实际上就是对参数列表进行拆分和组合并传递给其他函数)获得强类型验证。我们能够将元组类型用于剩余参数,正是这一点至关重要的场景之一。
例如,以下使用元组类型作为剩余参数的函数……
tsfunction foo(...args: [string, number]): void {// ...}
……对于 foo 的任何调用者来说,应该看起来与以下函数没有区别……
tsfunction foo(arg0: string, arg1: number): void {// ...}
……
tsTryfoo ("hello", 42);Expected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.foo ("hello", 42,true );Expected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.("hello"); foo
然而,差异开始变得显而易见的地方有一个:可读性。在第一个示例中,我们没有为第一个和第二个元素指定参数名称。虽然这些对类型检查没有影响,但元组位置缺乏标签会使它们更难使用——更难传达我们的意图。
这就是为什么在 TypeScript 4.0 中,元组类型现在可以提供标签。
tstype Range = [start: number, end: number];
为了加深参数列表和元组类型之间的联系,剩余元素和可选元素的语法与参数列表的语法保持一致。
tstype Foo = [first: number, second?: string, ...rest: any[]];
使用带标签的元组有一些规则。首先,当标记元组的一个元素时,元组中的所有其他元素也必须被标记。
tsTrytypeBar = [first : string, number];
值得注意的是,标签并不要求我们在解构时以不同方式命名变量。它们纯粹是为了文档和工具支持而存在的。
tsTryfunctionfoo (x : [first : string,second : number]) {// ...// note: we didn't need to name these 'first' and 'second'const [a ,b ] =x ;a b }
总的来说,带标签的元组在利用元组和参数列表相关的模式,以及以类型安全的方式实现重载时非常方便。事实上,TypeScript 的编辑器支持将尽可能尝试将它们显示为重载。

要了解更多信息,请查看带标签元组元素的拉取请求。
从构造函数推断类属性
当启用 noImplicitAny 时,TypeScript 4.0 现在可以使用控制流分析来确定类中属性的类型。
tsTryclassSquare {// Previously both of these were anyarea ;sideLength ;constructor(sideLength : number) {this.sideLength =sideLength ;this.area =sideLength ** 2;}}
在构造函数的路径没有全部为实例成员赋值的情况下,该属性被认为可能为 undefined。
tsTryclassSquare {sideLength ;constructor(sideLength : number) {if (Math .random ()) {this.sideLength =sideLength ;}}getarea () {return this.Object is possibly 'undefined'.2532Object is possibly 'undefined'.sideLength ** 2;}}
在你知道具体情况(例如,你有一个某种类型的 initialize 方法)的情况下,如果你开启了 strictPropertyInitialization,你仍然需要一个明确的类型注解以及一个明确的赋值断言(!)。
tsTryclassSquare {// definite assignment assertion// vsideLength !: number;// type annotationconstructor(sideLength : number) {this.initialize (sideLength );}initialize (sideLength : number) {this.sideLength =sideLength ;}getarea () {return this.sideLength ** 2;}}
更多详细信息,请参阅实现的拉取请求。
短路赋值运算符
JavaScript 和许多其他语言支持一组称为复合赋值运算符的运算符。复合赋值运算符将运算符应用于两个参数,然后将结果赋值给左侧。你可能之前见过这些:
ts// Addition// a = a + ba += b;// Subtraction// a = a - ba -= b;// Multiplication// a = a * ba *= b;// Division// a = a / ba /= b;// Exponentiation// a = a ** ba **= b;// Left Bit Shift// a = a << ba <<= b;
JavaScript 中的许多运算符都有相应的赋值运算符!然而直到最近,还有三个显著的例外:逻辑与(&&)、逻辑或(||)和空值合并(??)。
这就是为什么 TypeScript 4.0 支持一项新的 ECMAScript 功能,增加了三个新的赋值运算符:&&=、||= 和 ??=。
这些运算符非常适合替代用户可能编写如下代码的任何示例:
tsa = a && b;a = a || b;a = a ?? b;
或者类似的 if 块:
ts// could be 'a ||= b'if (!a) {a = b;}
我们甚至看到过(或者,呃,自己写过)一些模式来延迟初始化值,仅在需要时才进行。
tslet values: string[];(values ?? (values = [])).push("hello");// After(values ??= []).push("hello");
(看,我们对写出的所有代码并不都感到骄傲……)
在极少数情况下,如果你使用带有副作用的 getter 或 setter,值得注意的是,这些运算符仅在必要时才执行赋值。从这个意义上讲,不仅运算符的右侧是“短路”的——赋值本身也是如此。
tsobj.prop ||= foo();// roughly equivalent to either of the followingobj.prop || (obj.prop = foo());if (!obj.prop) {obj.prop = foo();}
尝试运行以下示例,看看这与始终执行赋值有何不同。
tsTryconstobj = {getprop () {console .log ("getter has run");// Replace me!returnMath .random () < 0.5;},setprop (_val : boolean) {console .log ("setter has run");}};functionfoo () {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 ();
我们要向社区成员 Wenlu Wang 表示衷心的感谢,感谢他的贡献!
有关更多详细信息,你可以在这里查看拉取请求。你也可以查看此功能的 TC39 提案仓库。
catch 子句绑定上的 unknown
自 TypeScript 诞生之初,catch 子句变量就被定义为 any 类型。这意味着 TypeScript 允许你对它们做任何你想做的事情。
tsTrytry {// Do some work} catch (x ) {// x has type 'any' - have fun!console .log (x .message );console .log (x .toUpperCase ());x ++;x .yadda .yadda .yadda ();}
如果我们试图防止在错误处理代码中出现更多错误,上述情况会有一些不良行为!因为这些变量默认具有 any 类型,它们缺乏任何类型安全,无法在无效操作上报错。
这就是为什么 TypeScript 4.0 现在允许你将 catch 子句变量的类型指定为 unknown。unknown 比 any 更安全,因为它提醒我们在操作值之前需要进行某种类型检查。
tsTrytry {// ...} catch (e : unknown) {// Can't access values on unknowns'e' is of type 'unknown'.18046'e' is of type 'unknown'.console .log (. e toUpperCase ());if (typeofe === "string") {// We've narrowed 'e' down to the type 'string'.console .log (e .toUpperCase ());}}
虽然 catch 变量的类型默认不会改变,但我们将来可能会考虑一个新的 strict 模式标志,以便用户可以选择此行为。在此期间,编写 lint 规则来强制 catch 变量具有 : any 或 : unknown 的显式注解是可能的。
有关更多详细信息,你可以预览此功能的更改。
自定义 JSX 工厂
在使用 JSX 时,片段(fragment)是一种允许我们返回多个子元素的 JSX 元素。当我们第一次在 TypeScript 中实现片段时,我们并不清楚其他库将如何利用它们。如今,大多数鼓励使用 JSX 并支持片段的其他库都有类似的 API 结构。
在 TypeScript 4.0 中,用户可以通过新的 jsxFragmentFactory 选项自定义片段工厂。
作为一个例子,以下 tsconfig.json 文件告诉 TypeScript 以与 React 兼容的方式转换 JSX,但将每个工厂调用切换为 h 而不是 React.createElement,并使用 Fragment 而不是 React.Fragment。
{"": {"": "esnext","": "commonjs","": "react","": "h","": "Fragment"}}
在需要为每个文件指定不同 JSX 工厂的情况下,你可以利用新的 /** @jsxFrag */ pragma 注释。例如,以下代码……
tsxTry// Note: these pragma comments need to be written// with a JSDoc-style multiline syntax to take effect./** @jsx h *//** @jsxFrag Fragment */import {h ,Fragment } from "preact";export constHeader = (<><h1 >Welcome</h1 ></>);
……将被转换为如下输出的 JavaScript……
tsxTryimport React from 'react';export const Header = (React.createElement(React.Fragment, null,React.createElement("h1", null, "Welcome")));
我们要向社区成员 Noj Vek 表示衷心的感谢,感谢他提交此拉取请求并耐心地与我们的团队合作。
你可以查看拉取请求以了解更多详细信息!
在带有 --noEmitOnError 的 build 模式下的速度提升
以前,在使用 noEmitOnError 标志时,在先前有错误的编译之后再次编译程序会非常慢。这是因为上一次编译中的任何信息都不会基于 noEmitOnError 标志缓存到 .tsbuildinfo 文件中。
TypeScript 4.0 改变了这一点,在这些场景中带来了巨大的速度提升,进而改进了 --build 模式场景(这意味着同时启用了 incremental 和 noEmitOnError)。
有关详细信息,请阅读拉取请求中的更多信息。
带有 --noEmit 的 --incremental
TypeScript 4.0 允许我们在使用 noEmit 标志的同时利用 incremental 编译。这在以前是不允许的,因为 incremental 需要生成 .tsbuildinfo 文件;然而,启用更快的增量构建的使用场景对于所有用户来说都非常重要。
有关更多详细信息,你可以参阅实现的拉取请求。
编辑器改进
TypeScript 编译器不仅为大多数主要编辑器中 TypeScript 本身的编辑体验提供支持,它还为 Visual Studio 系列编辑器及其他编辑器中的 JavaScript 体验提供支持。因此,我们的大部分工作集中在改进编辑器场景上——这是作为开发者你花费时间最多的地方。
在编辑器中使用新的 TypeScript/JavaScript 功能的方式会因编辑器而异,但
- Visual Studio Code 支持选择不同版本的 TypeScript。此外,还有 JavaScript/TypeScript Nightly 扩展以保持在最前沿(这通常非常稳定)。
- Visual Studio 2017/2019 拥有[上述 SDK 安装程序]和 MSBuild 安装。
- Sublime Text 3 支持选择不同版本的 TypeScript。
你可以查看支持 TypeScript 的编辑器部分列表,以了解更多关于你最喜欢的编辑器是否支持使用新版本的信息。
转换为可选链
可选链是一项最近受到广泛欢迎的功能。这就是为什么 TypeScript 4.0 引入了新的重构功能,以将常见模式转换为利用可选链和空值合并!

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

此新功能得益于 Wenlu Wang。有关更多详细信息,请参阅拉取请求。
启动时的部分语义模式
我们听到了许多关于启动时间过长的用户抱怨,特别是在大型项目中。罪魁祸首通常是一个称为程序构建的过程。这是从一组初始根文件开始,解析它们,查找它们的依赖项,解析这些依赖项,查找这些依赖项的依赖项,依此类推的过程。项目越大,你等待基本编辑器操作(如转到定义或快速信息)的时间就越长。
这就是为什么我们一直在为编辑器开发一种新模式,以便在完整的语言服务体验加载之前提供部分体验。核心思想是编辑器可以运行一个轻量级的部分服务器,该服务器仅查看编辑器当前打开的文件。
很难确切说你会看到什么样的改进,但据了解,TypeScript 在 Visual Studio Code 代码库上完全响应之前,通常需要20 秒到 1 分钟。相比之下,我们新的部分语义模式似乎将该延迟降低到了仅几秒钟。作为一个例子,在下面的视频中,你可以看到两个并排的编辑器,左侧运行的是 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 列出了超过 10 个尚未导入的类型化依赖项,此功能会自动禁用自身以防止项目加载变慢。要强制该功能工作,或完全禁用它,你应该能够配置你的编辑器。对于 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 的属性时,总是会发出错误。
tsTryclassBase {getfoo () {return 100;}setfoo (value ) {// ...}}classDerived extendsBase {'foo' is defined as an accessor in class 'Base', but is overridden here in 'Derived' as an instance property.2610'foo' is defined as an accessor in class 'Base', but is overridden here in 'Derived' as an instance property.= 10; foo }
tsTryclassBase {prop = 10;}classDerived extendsBase {get'prop' is defined as a property in class 'Base', but is overridden here in 'Derived' as an accessor.2611'prop' is defined as a property in class 'Base', but is overridden here in 'Derived' as an accessor.() { prop return 100;}}
有关更多详细信息,请参见实现的拉取请求。
delete 的操作数必须是可选的
在使用 strictNullChecks 时,使用 delete 运算符的操作数现在必须是 any、unknown、never,或者是可选的(即类型中包含 undefined)。否则,使用 delete 运算符将是一个错误。
tsTryinterfaceThing {prop : string;}functionf (x :Thing ) {deleteThe operand of a 'delete' operator must be optional.2790The operand of a 'delete' operator must be optional.x .prop ;}
有关更多详细信息,请参见实现的拉取请求。
弃用 TypeScript 的节点工厂(Node Factory)的使用
今天,TypeScript 提供了一组用于生成 AST 节点的“工厂”函数;然而,TypeScript 4.0 提供了一个新的节点工厂 API。因此,对于 TypeScript 4.0,我们决定弃用这些旧的函数,转而支持新的函数。
有关更多详细信息,请阅读有关此更改的相关拉取请求。
有关更多详细信息,你可以查看