TypeScript 4.2
更智能的类型别名保留
TypeScript 有一种为类型声明新名称的方法,称为类型别名。 如果你正在编写一组都作用于 string | number | boolean 的函数,你可以编写一个类型别名来避免重复。
type BasicPrimitive = number | string | boolean;TypeScript 在打印类型时,一直使用一套规则和猜测来决定何时重用类型别名。 例如,请看下面的代码片段。
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
let x = value;
return x;
}如果我们在像 Visual Studio、Visual Studio Code 或 TypeScript Playground 这样的编辑器中将鼠标悬停在 x 上,我们会看到一个快速信息面板,显示类型 BasicPrimitive。 同样,如果我们获取此文件的声明文件输出(.d.ts 输出),TypeScript 会说 doStuff 返回 BasicPrimitive。
但是,如果我们返回 BasicPrimitive 或 undefined 会怎样?
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
if (Math.random() < 0.5) {
return undefined;
}
return value;
}我们可以在 TypeScript 4.1 演练场。 虽然我们可能希望 TypeScript 将 doStuff 的返回类型显示为 BasicPrimitive | undefined,但它却显示为 string | number | boolean | undefined! 这是怎么回事?
这与 TypeScript 内部表示类型的方式有关。 当从一个或多个联合类型创建联合类型时,它总是会将这些类型标准化成一个新的扁平化联合类型——但这样做会丢失信息。 类型检查器必须从 string | number | boolean | undefined 中找到所有可能的类型组合,看看可能使用了哪些类型别名,即便如此,也可能有多个类型别名对应 string | number | boolean。
在 TypeScript 4.2 中,我们的内部机制更加智能。 我们通过保留类型最初编写和随时间构建的部分信息来跟踪类型的构建方式。 我们还跟踪并区分类型别名和其他别名的实例!
能够根据你在代码中使用类型的方式来打印类型,意味着作为 TypeScript 用户,你可以避免显示一些不幸的、过于庞大的类型,这通常会转化为更好的 .d.ts 文件输出、错误消息以及编辑器中快速信息和签名帮助里的类型显示。 这可以帮助 TypeScript 对新用户来说感觉更平易近人一些。
更多信息,请查看 第一个改进各种联合类型别名保留情况的拉取请求,以及 第二个保留间接别名的拉取请求。
元组类型中的前导/中间剩余元素
在 TypeScript 中,元组类型用于模拟具有特定长度和元素类型的数组。
// 存储一对数字的元组
let a: [number, number] = [1, 2];
// 存储一个字符串、一个数字和一个布尔值的元组
let b: [string, number, boolean] = ["hello", 42, true];随着时间的推移,TypeScript 的元组类型变得越来越复杂,因为它们也被用于模拟 JavaScript 中的参数列表等。 因此,它们可以有可选元素和剩余元素,甚至可以有标签以提高工具的可读性。
// 一个包含一个或两个字符串的元组。
let c: [string, string?] = ["hello"];
c = ["hello", "world"];
// 一个带有标签的元组,包含一个或两个字符串。
let d: [first: string, second?: string] = ["hello"];
d = ["hello", "world"];
// 一个带有*剩余元素*的元组 - 前面至少有两个字符串,
// 后面有任意数量的布尔值。
let e: [string, string, ...boolean[]];
e = ["hello", "world"];
e = ["hello", "world", false];
e = ["hello", "world", true, false, true];Try在 TypeScript 4.2 中,剩余元素的使用方式得到了扩展。 在之前的版本中,TypeScript 只允许 ...rest 元素位于元组类型的最后位置。
然而,现在剩余元素可以出现在元组中的任何位置——只有少数限制。
let foo: [...string[], number];
foo = [123];
foo = ["hello", 123];
foo = ["hello!", "hello!", "hello!", 123];
let bar: [boolean, ...string[], boolean];
bar = [true, false];
bar = [true, "some text", false];
bar = [true, "some", "separated", "text", false];Try唯一的限制是,剩余元素可以放置在元组中的任何位置,只要它后面不跟另一个可选元素或剩余元素。 换句话说,每个元组只有一个剩余元素,并且剩余元素之后不能有可选元素。
interface Clown {
/*...*/
}
interface Joker {
/*...*/
}
let StealersWheel: [...Clown[], "me", ...Joker[]];
let StringsAndMaybeBoolean: [...string[], boolean?];Try这些非尾随剩余元素可用于模拟接受任意数量的前导参数,后跟几个固定参数的函数。
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;
doStuff(/*shouldCapitalize:*/ false)
doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);Try尽管 JavaScript 没有语法来模拟前导剩余参数,但我们仍然可以通过使用带有前导剩余元素的元组类型来声明 ...args 剩余参数,从而将 doStuff 声明为接受前导参数的函数。 这可以帮助模拟大量现有的 JavaScript 代码!
更多细节,请查看原始拉取请求。
对 in 运算符的更严格检查
在 JavaScript 中,在 in 运算符的右侧使用非对象类型是运行时错误。 TypeScript 4.2 确保可以在设计时捕获这一点。
这种检查在大多数情况下相当保守,因此如果你收到关于此的错误,很可能是代码中的问题。
非常感谢我们的外部贡献者 Jonas Hübotter 的拉取请求!
--noPropertyAccessFromIndexSignature
早在 TypeScript 首次引入索引签名时,你只能通过“括号”元素访问语法(如 person["name"])来获取它们声明的属性。
interface SomeType {
/** 这是一个索引签名。 */
[propName: string]: any;
}
function doStuff(value: SomeType) {
let x = value["someProperty"];
}Try在我们需要处理具有任意属性的对象的情况下,这最终变得很麻烦。 例如,想象一个 API,其中常见的情况是通过在末尾添加额外的 s 字符来拼错属性名称。
interface Options {
/** 要排除的文件模式。 */
exclude?: string[];
/**
* 它处理我们尚未声明为类型 'any' 的任何额外属性。
*/
[x: string]: any;
}
function processOptions(opts: Options) {
// 注意我们是*故意*访问 `excludes`,而不是 `exclude`
if (opts.excludes) {
console.error(
"选项 `excludes` 无效。您是想用 `exclude` 吗?"
);
}
}Try为了使这些类型的情况更容易处理,一段时间前,当类型具有字符串索引签名时,TypeScript 使得使用“点”属性访问语法(如 person.name)成为可能。 这也使得将现有 JavaScript 代码迁移到 TypeScript 变得更加容易。
然而,放宽限制也意味着拼错显式声明的属性变得更容易了。
function processOptions(opts: Options) {
// ...
// 注意这次我们是*无意中*访问了 `excludes`。
// 哎呀!完全有效。
for (const excludePattern of opts.excludes) {
// ...
}
}Try在某些情况下,用户更愿意显式选择使用索引签名——他们希望当点属性访问不对应于特定属性声明时收到错误消息。
这就是 TypeScript 引入一个名为 noPropertyAccessFromIndexSignature 的新标志的原因。 在此模式下,你将选择使用 TypeScript 的旧行为,即发出错误。 这个新设置不属于 strict 系列标志,因为我们认为用户会在某些代码库中发现它比其他代码库更有用。
你可以通过阅读相应的拉取请求来详细了解此功能。 我们还要非常感谢 Wenlu Wang 向我们提交了这个拉取请求!
abstract 构造签名
TypeScript 允许我们将一个类标记为 abstract。 这告诉 TypeScript 该类仅用于被扩展,并且任何子类需要填充某些成员才能实际创建实例。
abstract class Shape {
abstract getArea(): number;
}
new Shape();
class Square extends Shape {
#sideLength: number;
constructor(sideLength: number) {
super();
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
}
// 正常工作。
new Square(42);Try为了确保对 abstract 类进行 new 操作的限制得到一致应用,你不能将 abstract 类赋值给任何期望构造签名的东西。
如果我们打算运行像 new Ctor 这样的代码,这样做是正确的,但如果我们想编写 Ctor 的子类,这就过于严格了。
abstract class Shape {
abstract getArea(): number;
}
interface HasArea {
getArea(): number;
}
function makeSubclassWithArea(Ctor: new () => HasArea) {
return class extends Ctor {
getArea() {
return 42
}
};
}
let MyShape = makeSubclassWithArea(Shape);Try它也不能很好地与内置辅助类型(如 InstanceType)配合使用。
这就是为什么 TypeScript 4.2 允许你在构造函数签名上指定 abstract 修饰符。
将 abstract 修饰符添加到构造签名表示你可以传入 abstract 构造函数。 它不会阻止你传入其他“具体”的类/构造函数——它实际上只是表示没有意图直接运行构造函数,因此传入任何一种类类型都是安全的。
此功能允许我们以支持抽象类的方式编写 mixin 工厂。 例如,在下面的代码片段中,我们能够将 mixin 函数 withStyles 与 abstract 类 SuperClass 一起使用。
abstract class SuperClass {
abstract someMethod(): void;
badda() {}
}
type AbstractConstructor<T> = abstract new (...args: any[]) => T
function withStyles<T extends AbstractConstructor<object>>(Ctor: T) {
abstract class StyledClass extends Ctor {
getStyles() {
// ...
}
}
return StyledClass;
}
class SubClass extends withStyles(SuperClass) {
someMethod() {
this.someMethod()
}
}Try注意 withStyles 演示了一个特定规则,即一个类(如 StyledClass)如果扩展了一个值是泛型且受抽象构造函数约束(如 Ctor)的值,那么它也必须被声明为 abstract。 这是因为无法知道传入的类是否具有更多抽象成员,因此无法知道子类是否实现了所有抽象成员。
你可以在其拉取请求上阅读更多关于抽象构造签名的信息。
使用 --explainFiles 理解你的项目结构
对于 TypeScript 用户来说,一个出奇常见的场景是问“为什么 TypeScript 要包含这个文件?”。 推断程序的文件是一个复杂的过程,因此有很多原因导致使用了特定的 lib.d.ts 组合,为什么 node_modules 中的某些文件被包含进来,以及为什么即使我们认为指定了 exclude 可以排除它们,某些文件仍然被包含进来。
这就是为什么 TypeScript 现在提供了一个 explainFiles 标志。
tsc --explainFiles使用此选项时,TypeScript 编译器将提供非常详细的输出,说明文件最终进入程序的原因。 为了更容易阅读,你可以将输出转发到一个文件,或将其传递给一个可以轻松查看它的程序。
# 将输出转发到文本文件
tsc --explainFiles > explanation.txt
# 将输出传递给像 `less` 这样的实用程序,或像 VS Code 这样的编辑器
tsc --explainFiles | less
tsc --explainFiles | code -通常,输出将首先列出包含 lib.d.ts 文件的原因,然后是本地文件,然后是 node_modules 文件。
TS_Compiler_Directory/4.2.2/lib/lib.es5.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts' 引用的库 'es5'
TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts' 引用的库 'es2015'
TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts' 引用的库 'es2016'
TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts' 引用的库 'es2017'
TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts' 引用的库 'es2018'
TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts' 引用的库 'es2019'
TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts
通过文件 'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts' 引用的库 'es2020'
TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts
在 compilerOptions 中指定的库 'lib.esnext.d.ts'
... 更多库引用 ...
foo.ts
匹配了 'tsconfig.json' 中的包含模式 '**/*'目前,我们对输出格式不作任何保证——它将来可能会改变。 关于这一点,如果你有任何建议,我们有兴趣改进这种格式!
更多信息,请查看原始拉取请求!
逻辑表达式中未调用函数检查的改进
感谢 Alex Tarasyuk 的进一步改进,TypeScript 的未调用函数检查现在适用于 && 和 || 表达式。
在 strictNullChecks 下,以下代码现在会报错。
function shouldDisplayElement(element: Element) {
// ...
return true;
}
function getVisibleItems(elements: Element[]) {
return elements.filter((e) => shouldDisplayElement && e.children.length);
// ~~~~~~~~~~~~~~~~~~~~
// 此条件将始终返回 true,因为该函数始终被定义。
// 您是否打算调用它?
}更多细节,请在此处查看拉取请求。
解构变量可以显式标记为未使用
感谢 Alex Tarasyuk 的另一个拉取请求,你现在可以通过在解构变量前加上下划线(_ 字符)来将它们标记为未使用。
let [_first, second] = getValues();以前,如果 _first 之后从未被使用,TypeScript 会在 noUnusedLocals 下报错。 现在,TypeScript 将识别出 _first 是有意用下划线命名的,因为没有打算使用它。
更多细节,请查看完整的更改。
可选属性和字符串索引签名之间的规则放宽
字符串索引签名是一种为类似字典的对象添加类型的方法,允许使用任意键进行访问:
const movieWatchCount: { [key: string]: number } = {};
function watchMovie(title: string) {
movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}Try当然,对于字典中尚未出现的任何电影标题,movieWatchCount[title] 将是 undefined(TypeScript 4.1 添加了 noUncheckedIndexedAccess 选项,以便在从这样的索引签名读取时包含 undefined)。 尽管很明显 movieWatchCount 中必定缺少某些字符串,但 TypeScript 的先前版本将可选对象属性视为与原本兼容的索引签名不可赋值,因为存在 undefined。
type WesAndersonWatchCount = {
"Fantastic Mr. Fox"?: number;
"The Royal Tenenbaums"?: number;
"Moonrise Kingdom"?: number;
"The Grand Budapest Hotel"?: number;
};
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ 错误!
// 类型 'WesAndersonWatchCount' 不能赋值给类型 '{ [key: string]: number; }'。
// 属性 '"Fantastic Mr. Fox"' 与索引签名不兼容。
// 类型 'number | undefined' 不能赋值给类型 'number'。
// 类型 'undefined' 不能赋值给类型 'number'。(2322)TryTypeScript 4.2 允许这种赋值。但是,它不允许在其类型中带有 undefined 的非可选属性的赋值,也不允许将 undefined 写入特定键:
type BatmanWatchCount = {
"Batman Begins": number | undefined;
"The Dark Knight": number | undefined;
"The Dark Knight Rises": number | undefined;
};
declare const batmanWatchCount: BatmanWatchCount;
// 在 TypeScript 4.2 中仍然是一个错误。
const movieWatchCount: { [key: string]: number } = batmanWatchCount;
// 在 TypeScript 4.2 中仍然是一个错误。
// 索引签名不隐式允许显式的 `undefined`。
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;Try新规则也不适用于数字索引签名,因为它们被认为是类似数组且密集的:
declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };
sortOfArrayish = numberKeys;Try你可以通过阅读原始 PR 更好地了解此更改。
声明缺失的辅助函数
感谢 Alexander Tarasyuk 的社区拉取请求,我们现在有了一个基于调用站点声明新函数和方法的快速修复!

重大更改
我们始终努力在发布中尽量减少重大更改。 TypeScript 4.2 包含一些重大更改,但我们相信在升级时它们应该是可控的。
lib.d.ts 更新
与每个 TypeScript 版本一样,lib.d.ts 的声明(尤其是为 Web 上下文生成的声明)已经更改。 有各种更改,但 Intl 和 ResizeObserver 的更改可能是最具破坏性的。
noImplicitAny 错误适用于松散的 yield 表达式
当捕获 yield 表达式的值,但 TypeScript 无法立即确定你希望它接收什么类型时(即 yield 表达式没有上下文类型),TypeScript 现在将发出隐式 any 错误。
function* g1() {
const value = yield 1;}
function* g2() {
// 没有错误。
// `yield 1` 的结果未被使用。
yield 1;
}
function* g3() {
// 没有错误。
// `yield 1` 被 'string' 上下文类型化。
const value: string = yield 1;
}
function* g4(): Generator<number, void, string> {
// 没有错误。
// TypeScript 可以从 `g4` 的显式返回类型中找出 `yield 1` 的类型。
const value = yield 1;
}Try在相应的更改中查看更多详细信息。
扩展的未调用函数检查
如上所述,当使用 strictNullChecks 时,未调用函数检查现在将在 && 和 || 表达式中一致地运行。 这可能会带来新的中断,但通常表明现有代码中存在逻辑错误。
JavaScript 中的类型参数不被解析为类型参数
在 JavaScript 中,类型参数本来就不被允许,但在 TypeScript 4.2 中,解析器将以更符合规范的方式解析它们。 因此,在 JavaScript 文件中编写以下代码时:
f<T>(100);TypeScript 将把它解析为以下 JavaScript:
f < T > 100;如果你利用 TypeScript 的 API 来解析 JavaScript 文件中的类型构造(例如在尝试解析 Flow 文件时),这可能会影响你。
有关检查内容的更多详细信息,请参阅拉取请求。
展开的元组大小限制
元组类型可以通过在 TypeScript 中使用任何类型的展开语法(...)来创建。
// 带有展开元素的元组类型
type NumStr = [number, string];
type NumStrNumStr = [...NumStr, ...NumStr];
// 数组展开表达式
const numStr = [123, "hello"] as const;
const numStrNumStr = [...numStr, ...numStr] as const;有时这些元组类型可能会意外地变得非常大,这可能会使类型检查花费很长时间。 为了避免让类型检查过程挂起(这在编辑器场景中尤其糟糕),TypeScript 设置了一个限制器来避免进行所有工作。
你可以查看此拉取请求了解更多详细信息。
不能在导入路径中使用 .d.ts 扩展名
在 TypeScript 4.2 中,现在导入路径包含 .d.ts 扩展名是错误的。
// 必须更改为类似
// - "./foo"
// - "./foo.js"
import { Foo } from "./foo.d.ts";相反,你的导入路径应该反映你的加载器在运行时的行为。 以下任何导入都可能可用。
import { Foo } from "./foo";
import { Foo } from "./foo.js";
import { Foo } from "./foo/index.js";恢复模板字面量推断
此更改从 TypeScript 4.2 beta 中移除了一个功能。 如果你尚未升级到我们最新的稳定版本,你不会受到影响,但你可能仍然对此更改感兴趣。
TypeScript 4.2 的 beta 版本包含了对模板字符串推断的更改。 在此更改中,模板字符串字面量要么被赋予模板字符串类型,要么被简化为多个字符串字面量类型。 然后,当赋值给可变变量时,这些类型会拓宽为 string。
declare const yourName: string;
// 'bar' 是常量。
// 它的类型是 '`hello ${string}`'。
const bar = `hello ${yourName}`;
// 'baz' 是可变的。
// 它的类型是 'string'。
let baz = `hello ${yourName}`;这类似于字符串字面量推断的工作方式。
// 'bar' 的类型是 '"hello"'。
const bar = "hello";
// 'baz' 的类型是 'string'。
let baz = "hello";因此,我们相信让模板字符串表达式具有模板字符串类型会是“一致的”; 然而,根据我们所看到和听到的,这并不总是可取的。
作为回应,我们已经恢复了这个功能(以及潜在的破坏性更改)。 如果你确实希望模板字符串表达式具有类似字面量的类型,你可以在其末尾添加 as const。
declare const yourName: string;
// 'bar' 的类型是 '`hello ${string}`'。
const bar = `hello ${yourName}` as const;
// ^^^^^^^^
// 'baz' 的类型是 'string'。
const baz = `hello ${yourName}`;TypeScript 在 visitNode 中的 lift 回调使用了不同的类型
TypeScript 有一个 visitNode 函数,它接受一个 lift 函数。 lift 现在期望一个 readonly Node[] 而不是 NodeArray<Node>。 这严格来说是一个 API 的破坏性更改,你可以在此处阅读更多信息。