TypeScript 4.4

别名条件和判别式的控制流分析

在 JavaScript 中,我们经常需要以不同的方式探查一个值,并根据其类型的进一步信息执行不同的操作。TypeScript 理解这些检查,并将它们称为类型保护(type guards)。类型检查器利用所谓的控制流分析(control flow analysis)来查看我们在给定代码段之前是否使用了类型保护,而不是要求我们在每次使用变量时都向 TypeScript 证明其类型。

例如,我们可以编写如下代码:

ts
function foo(arg: unknown) {
if (typeof arg === "string") {
console.log(arg.toUpperCase());
(parameter) arg: string
}
}
Try

在此示例中,我们检查了 arg 是否为 string。TypeScript 识别出 typeof arg === "string" 这个检查,将其视为一个类型保护,并知道在 if 块内 arg 是一个 string。这让我们能够访问 string 的方法(如 toUpperCase())而不会报错。

但是,如果我们把条件移动到一个名为 argIsString 的常量中会发生什么呢?

ts
// In TS 4.3 and below
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// Error! Property 'toUpperCase' does not exist on type 'unknown'.
}
}

在之前的 TypeScript 版本中,这会产生错误——尽管 argIsString 被赋值为一个类型保护,但 TypeScript 丢失了该信息。这很遗憾,因为我们可能希望在多个地方重复使用同一个检查。为了解决这个问题,用户往往不得不重复代码或使用类型断言(即类型转换)。

在 TypeScript 4.4 中,情况不再如此。上述示例可以正常工作且没有错误!当 TypeScript 看到我们在测试一个常量值时,它会进行额外的工作,查看该常量是否包含类型保护。如果该类型保护作用于 constreadonly 属性或未修改的参数,那么 TypeScript 就能够适当地收窄(narrow)该值。

不仅是 typeof 检查,不同类型的类型保护条件都能被保留。例如,对判别式联合类型(discriminated unions)的检查也能完美运行。

ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
 
function area(shape: Shape): number {
const isCircle = shape.kind === "circle";
if (isCircle) {
// We know we have a circle here!
return Math.PI * shape.radius ** 2;
} else {
// We know we're left with a square here!
return shape.sideLength ** 2;
}
}
Try

4.4 中对判别式的分析也更深入了——我们现在可以将判别式提取出来,TypeScript 依然能够对原始对象进行收窄。

ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
 
function area(shape: Shape): number {
// Extract out the 'kind' field first.
const { kind } = shape;
 
if (kind === "circle") {
// We know we have a circle here!
return Math.PI * shape.radius ** 2;
} else {
// We know we're left with a square here!
return shape.sideLength ** 2;
}
}
Try

作为另一个例子,这是一个检查其两个输入是否包含内容的方法:

ts
function doSomeChecks(
inputA: string | undefined,
inputB: string | undefined,
shouldDoExtraWork: boolean
) {
const mustDoWork = inputA && inputB && shouldDoExtraWork;
if (mustDoWork) {
// We can access 'string' properties on both 'inputA' and 'inputB'!
const upperA = inputA.toUpperCase();
const upperB = inputB.toUpperCase();
// ...
}
}
Try

如果 mustDoWorktrue,TypeScript 可以理解 inputAinputB 同时存在。这意味着我们不必编写非空断言(如 inputA!)来向 TypeScript 证明 inputA 不是 undefined

这里一个很棒的功能是,这种分析是传递性的。TypeScript 会通过常量进行跳转,以理解你已经执行了哪些检查。

ts
function f(x: string | number | boolean) {
const isString = typeof x === "string";
const isNumber = typeof x === "number";
const isStringOrNumber = isString || isNumber;
if (isStringOrNumber) {
x;
(parameter) x: string | number
} else {
x;
(parameter) x: boolean
}
}
Try

注意,这里有一个截止点——TypeScript 在检查这些条件时不会进行无限深的嵌套,但其分析深度对于大多数检查来说已经足够了。

这个功能应该能让许多直观的 JavaScript 代码在 TypeScript 中“直接运行”,而不会产生阻碍。更多详细信息,请查看 GitHub 上的实现

Symbol 和模板字符串模式索引签名

TypeScript 允许我们使用索引签名(index signatures)来描述对象,其中每个属性都必须具有特定的类型。这允许我们将这些对象用作字典类类型,我们可以使用字符串键通过方括号对其进行索引。

例如,我们可以编写一个带有索引签名的类型,它接收 string 键并映射到 boolean 值。如果我们尝试分配除 boolean 以外的任何值,就会收到错误。

ts
interface BooleanDictionary {
[key: string]: boolean;
}
 
declare let myDict: BooleanDictionary;
 
// Valid to assign boolean values
myDict["foo"] = true;
myDict["bar"] = false;
 
// Error, "oops" isn't a boolean
myDict["baz"] = "oops";
Type 'string' is not assignable to type 'boolean'.2322Type 'string' is not assignable to type 'boolean'.
Try

虽然 Map 在这里可能是一个更好的数据结构(特别是 Map<string, boolean>),但 JavaScript 对象通常更易于使用,或者恰好就是我们需要处理的数据形式。

同样地,Array<T> 已经定义了一个 number 索引签名,允许我们插入/检索类型为 T 的值。

ts
// @errors: 2322 2375
// This is part of TypeScript's definition of the built-in Array type.
interface Array<T> {
[index: number]: T;
// ...
}
let arr = new Array<string>();
// Valid
arr[0] = "hello!";
// Error, expecting a 'string' value here
arr[1] = 123;

索引签名对于表达大量的实际代码非常有用;然而,到目前为止,它们仅限于 stringnumber 键(并且 string 索引签名有一个特意的特性,即它们可以接受 number 键,因为数字会被自动转换为字符串)。这意味着 TypeScript 不允许使用 symbol 键对对象进行索引。TypeScript 也无法对 string 键的某个子集建立索引签名——例如,描述名称以 data- 开头的属性的索引签名。

TypeScript 4.4 解决了这些限制,并允许针对 symbol 和模板字符串模式使用索引签名。

例如,TypeScript 现在允许我们声明一个可以使用任意 symbol 作为键的类型。

ts
interface Colors {
[sym: symbol]: number;
}
 
const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");
 
let colors: Colors = {};
 
// Assignment of a number is allowed
colors[red] = 255;
let redVal = colors[red];
let redVal: number
 
colors[blue] = "da ba dee";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

类似地,我们可以编写带有模板字符串模式类型的索引签名。这的一种用途是可以将以 data- 开头的属性从 TypeScript 的多余属性检查(excess property checking)中排除。当我们向具有预期类型的对象字面量赋值时,TypeScript 会查找预期类型中未声明的多余属性。

ts
// @errors: 2322 2375
interface Options {
width?: number;
height?: number;
}
let a: Options = {
width: 100,
height: 100,
"data-blah": true,
};
interface OptionsWithDataProps extends Options {
// Permit any property starting with 'data-'.
[optName: `data-${string}`]: unknown;
}
let b: OptionsWithDataProps = {
width: 100,
height: 100,
"data-blah": true,
// Fails for a property which is not known, nor
// starts with 'data-'
"unknown-property": true,
};

关于索引签名的最后一点说明是,它们现在允许联合类型,只要它们是无限域原始类型的联合——具体来说:

  • string
  • number
  • symbol
  • 模板字符串模式(例如 `hello-${string}`

参数为这些类型联合的索引签名将被解构为多个不同的索引签名。

ts
interface Data {
[optName: string | symbol]: any;
}
// Equivalent to
interface Data {
[optName: string]: any;
[optName: symbol]: any;
}

有关详细信息,请阅读相关 PR

Catch 变量默认使用 unknown 类型 (--useUnknownInCatchVariables)

在 JavaScript 中,任何类型的值都可以通过 throw 抛出并在 catch 子句中被捕获。因此,TypeScript 历史上将 catch 子句变量的类型设为 any,并且不允许任何其他类型注解。

ts
try {
// Who knows what this might throw...
executeSomeThirdPartyCode();
} catch (err) {
// err: any
console.error(err.message); // Allowed, because 'any'
err.thisWillProbablyFail(); // Allowed, because 'any' :(
}

自从 TypeScript 添加了 unknown 类型后,很明显 unknownany 对于 catch 子句变量是更好的选择(对于追求最高程度正确性和类型安全性的用户而言),因为它更容易收窄,并强制我们对任意值进行测试。最终,TypeScript 4.0 允许用户在每个 catch 子句变量上显式指定 unknown(或 any)类型注解,以便我们可以按需选择更严格的类型;然而,对于某些人来说,在每个 catch 子句上手动指定 : unknown 是一件繁琐的工作。

这就是为什么 TypeScript 4.4 引入了一个名为 useUnknownInCatchVariables 的新标志。该标志将 catch 子句变量的默认类型从 any 更改为 unknown

ts
try {
executeSomeThirdPartyCode();
} catch (err) {
// err: unknown
 
// Error! Property 'message' does not exist on type 'unknown'.
console.error(err.message);
'err' is of type 'unknown'.18046'err' is of type 'unknown'.
 
// Works! We can narrow 'err' from 'unknown' to 'Error'.
if (err instanceof Error) {
console.error(err.message);
}
}
Try

此标志在 strict 选项系列下启用。这意味着如果你使用 strict 检查代码,该选项将自动开启。你在 TypeScript 4.4 中可能会遇到如下错误:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

在不希望处理 catch 子句中的 unknown 变量的情况下,我们始终可以添加显式的 : any 注解来放弃更严格的类型。

ts
try {
executeSomeThirdPartyCode();
} catch (err: any) {
console.error(err.message); // Works again!
}
Try

有关更多信息,请查看相关实现 PR

精确可选属性类型 (--exactOptionalPropertyTypes)

在 JavaScript 中,读取对象上缺失的属性会产生 undefined 值。同时,也可能确实存在一个值为 undefined 的属性。许多 JavaScript 代码倾向于将这两种情况同等对待,因此最初 TypeScript 只是将每个可选属性解释为用户在类型中编写了 undefined。例如:

ts
interface Person {
name: string;
age?: number;
}

被视为等同于:

ts
interface Person {
name: string;
age?: number | undefined;
}

这意味着用户可以显式地将 undefined 写入 age 的位置。

ts
const p: Person = {
name: "Daniel",
age: undefined, // This is okay by default.
};

因此,默认情况下,TypeScript 不区分属性存在但值为 undefined 和属性完全缺失这两种情况。虽然这在大多数情况下有效,但并非所有的 JavaScript 代码都做相同的假设。像 Object.assignObject.keys、对象展开({ ...obj })和 for-in 循环等函数和运算符,会根据属性是否真正存在于对象上而表现不同。在我们的 Person 示例中,如果 age 属性在对其存在性敏感的上下文中被观察,这可能会导致运行时错误。

在 TypeScript 4.4 中,新标志 exactOptionalPropertyTypes 指定可选属性类型应完全按书写方式解释,这意味着 | undefined 不会自动添加到该类型中。

ts
// With 'exactOptionalPropertyTypes' on:
const p: Person = {
Type '{ name: string; age: undefined; }' is not assignable to type 'Person' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'age' are incompatible. Type 'undefined' is not assignable to type 'number'.2375Type '{ name: string; age: undefined; }' is not assignable to type 'Person' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'age' are incompatible. Type 'undefined' is not assignable to type 'number'.
name: "Daniel",
age: undefined, // Error! undefined isn't a number
};
Try

此标志不是 strict 系列的一部分,如果您希望此行为,需要显式开启。它还要求同时启用 strictNullChecks。我们一直在对 DefinitelyTyped 和其他定义进行更新,以尽可能使过渡直接,但根据您的代码结构,您可能会遇到一些阻力。

有关更多信息,您可以查看此处的实现 PR

类中的 static

TypeScript 4.4 带来了对类中 static的支持,这是一项即将推出的 ECMAScript 功能,可以帮助您为静态成员编写更复杂的初始化代码。

ts
class Foo {
static count = 0;
 
// This is a static block:
static {
if (someCondition()) {
Foo.count++;
}
}
}
Try

这些静态块允许您编写一系列具有自身作用域的语句,这些语句可以访问包含类中的私有字段。这意味着我们可以编写具有语句编写所有功能、不会泄露变量且能完全访问类内部的初始化代码。

ts
class Foo {
static #count = 0;
 
get count() {
return Foo.#count;
}
 
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
Try

如果没有 static 块,编写上述代码是可能的,但通常涉及几种不同类型的黑客手段,必须在某些方面做出妥协。

请注意,一个类可以有多个 static 块,它们按照书写的顺序执行。

ts
// Prints:
// 1
// 2
// 3
class Foo {
static prop = 1
static {
console.log(Foo.prop++);
}
static {
console.log(Foo.prop++);
}
static {
console.log(Foo.prop++);
}
}
Try

我们要感谢 Wenlu Wang 为 TypeScript 实现了此功能。更多详细信息,您可以查看该 PR

tsc --help 更新与改进

TypeScript 的 --help 选项焕然一新!感谢 Song Gao 的部分贡献,我们对编译器选项的描述进行了更新,并以颜色和其他视觉分隔重新设计了 --help 菜单

The new TypeScript --help menu where the output is bucketed into several different areas

您可以在原始提案讨论帖中阅读更多内容。

性能改进

更快的声明文件生成

TypeScript 现在会缓存内部符号在不同上下文中的可访问性,以及特定类型的打印方式。这些更改可以提升 TypeScript 在处理具有相当复杂类型的代码时的总体性能,在使用 declaration 标志生成 .d.ts 文件时尤其明显。

在此处查看更多详细信息。.

更快的路径规范化

TypeScript 通常需要对文件路径执行多种“规范化”以使其成为编译器在各处都能使用的统一格式。这涉及诸如将反斜杠替换为斜杠,或移除路径中中间的 /.//../ 段等操作。当 TypeScript 需要处理数百万个此类路径时,这些操作会变得有点缓慢。在 TypeScript 4.4 中,路径首先会经过快速检查,以查看是否根本不需要规范化。这些改进共同使大型项目的加载时间缩短了 5-10%,在我们内部测试的超大规模项目中,提升幅度更是显著。

有关更多详细信息,您可以查看路径段规范化的 PR 以及斜杠规范化的 PR

更快的路径映射

TypeScript 现在缓存其构建路径映射(使用 tsconfig.json 中的 paths 选项)的方式。对于具有几百个映射的项目,减少量是显着的。您可以在变更本身中看到更多信息。

更快的 --strict 增量构建

作为一个实际上是 bug 的问题,TypeScript 会在开启 strict 的情况下,在 incremental 编译期间重复进行类型检查工作。这导致许多构建的速度与关闭 incremental 时一样慢。TypeScript 4.4 修复了这个问题,该更改也已向后移植到 TypeScript 4.3。

查看更多此处

针对大型输出更快的源码映射生成

TypeScript 4.4 为超大输出文件的源码映射生成添加了一项优化。在构建旧版本的 TypeScript 编译器时,这使得发射(emit)时间减少了约 8%。

我们要感谢 David Michon,他提供了一个简单且干净的更改来实现这一性能提升。

更快的 --force 构建

当对项目引用使用 --build 模式时,TypeScript 必须执行最新性检查以确定哪些文件需要重新构建。然而,执行 --force 构建时,这些信息是无关紧要的,因为每个项目依赖都将从头开始重新构建。在 TypeScript 4.4 中,--force 构建避免了这些不必要的步骤并开始完全构建。查看更多关于该更改的内容此处

JavaScript 的拼写建议

TypeScript 支持 Visual Studio 和 Visual Studio Code 等编辑器中的 JavaScript 编辑体验。大多数时候,TypeScript 会尽量不去打扰 JavaScript 文件;然而,TypeScript 通常拥有大量信息来进行自信的建议,并以不那么“侵入式”的方式呈现建议。

这就是为什么 TypeScript 现在在纯 JavaScript 文件中发布拼写建议——那些没有 // @ts-check 或项目中关闭了 checkJs 的文件。这些与 TypeScript 文件中已有的“你的意思是……”建议相同,现在它们以某种形式提供给所有 JavaScript 文件。

这些拼写建议可以提供一个细微的线索,表明您的代码可能有错。我们在测试此功能时,在现有代码中发现了一些 bug!

有关此新功能的更多详细信息,请查看 PR

内嵌提示 (Inlay Hints)

TypeScript 4.4 提供了对内嵌提示的支持,这有助于在代码中显示有用的信息,如参数名称和返回类型。您可以将其视为一种友好的“幽灵文本”。

A preview of inlay hints in Visual Studio Code

此功能由 Wenlu Wang 构建,其PR 包含更多详细信息。

Wenlu 还贡献了Visual Studio Code 中内嵌提示的集成,该集成已作为2021 年 7 月 (1.59) 版本的一部分发布。如果您想尝试内嵌提示,请确保您使用的是编辑器的最新稳定版预览版 (insiders)。您还可以在 Visual Studio Code 的设置中修改何时何地显示内嵌提示。

自动导入在补全列表中显示真实路径

当像 Visual Studio Code 这样的编辑器显示补全列表时,包含自动导入的补全会显示给定模块的路径;然而,此路径通常不是 TypeScript 最终放入模块标识符(module specifier)中的内容。该路径通常是相对于工作区的,这意味着如果您从像 moment 这样的包中导入,您通常会看到类似 node_modules/moment 的路径。

A completion list containing unwieldy paths containing 'node_modules'. For example, the label for 'calendarFormat' is 'node_modules/moment/moment' instead of 'moment'.

这些路径最终显得笨拙且往往具有误导性,特别是考虑到实际插入到您文件中的路径需要考虑 Node 的 node_modules 解析、路径映射、符号链接和重新导出。

这就是为什么在 TypeScript 4.4 中,补全项标签现在显示将用于导入的实际模块路径!

A completion list containing clean paths with no intermediate 'node_modules'. For example, the label for 'calendarFormat' is 'moment' instead of 'node_modules/moment/moment'.

由于此计算可能开销较大,包含许多自动导入的补全列表可能会在您键入更多字符时分批填充最终的模块标识符。您可能有时仍会看到旧的工作区相对路径标签;然而,随着您的编辑体验“预热”,它们应该会在再敲击一两个键后替换为实际路径。

破坏性变更

TypeScript 4.4 的 lib.d.ts 更改

与每个 TypeScript 版本一样,lib.d.ts 的声明(特别是为 Web 上下文生成的声明)发生了变化。您可以查阅我们已知的 lib.dom.d.ts 更改列表以了解哪些受到了影响。

针对导入函数的更合规的间接调用

在早期版本的 TypeScript 中,调用 CommonJS、AMD 和其他非 ES 模块系统的导入会设置所调用函数的 this 值。具体来说,在以下示例中,调用 fooModule.foo() 时,foo() 方法会将 fooModule 设置为 this 的值。

ts
// Imagine this is our imported module, and it has an export named 'foo'.
let fooModule = {
foo() {
console.log(this);
},
};
fooModule.foo();

这并不是 ECMAScript 中导出函数在我们调用它们时应有的工作方式。这就是为什么 TypeScript 4.4 通过使用以下发射方式,有意在调用导入函数时丢弃 this 值。

ts
// Imagine this is our imported module, and it has an export named 'foo'.
let fooModule = {
foo() {
console.log(this);
},
};
// Notice we're actually calling '(0, fooModule.foo)' now, which is subtly different.
(0, fooModule.foo)();

您可以在此处阅读更多关于这些更改的信息

在 Catch 变量中使用 unknown

使用 strict 标志运行的用户可能会看到关于 catch 变量为 unknown 的新错误,特别是如果现有代码假设只有 Error 值被捕获。这通常会导致如下错误消息:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

为了绕过这个问题,您可以专门添加运行时检查以确保抛出的类型与您预期的类型匹配。否则,您可以使用类型断言,为您的 catch 变量添加显式的 : any,或者关闭 useUnknownInCatchVariables

更广泛的 Always-Truthy Promise 检查

在之前的版本中,TypeScript 引入了“Always Truthy Promise 检查”来捕捉可能忘记了 await 的代码;然而,这些检查仅适用于命名声明。这意味着虽然这段代码会正确收到错误……

ts
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
const fooResult = foo();
if (fooResult) {
// <- error! :D
return "true";
}
return "false";
}

……但以下代码则不会。

ts
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) {
// <- no error :(
return "true";
}
return "false";
}

TypeScript 4.4 现在会将两者都标记出来。有关更多信息,请阅读原始更改信息

抽象属性不允许使用初始化器

以下代码现在会产生错误,因为抽象属性不能有初始化器:

ts
abstract class C {
abstract prop = 1;
// ~~~~
// Property 'prop' cannot have an initializer because it is marked abstract.
}

相反,您只能为该属性指定一个类型:

ts
abstract class C {
abstract prop: number;
}

TypeScript 文档是一个开源项目。通过发送 Pull Request 帮助我们改进这些页面 ❤

此页面的贡献者
OTOrta Therox (2)
ELEliran Levi (1)
ABAndrew Branch (1)
JBJack Bates (1)

最后更新:2026 年 3 月 27 日