更智能的类型别名保留
TypeScript 有一种为类型声明新名称的方法,称为类型别名。如果你正在编写一组都适用于 string | number | boolean 的函数,你可以编写一个类型别名来避免反复书写相同的代码。
tstype BasicPrimitive = number | string | boolean;
TypeScript 在打印输出类型时,一直使用一套规则和推测来决定何时重用类型别名。例如,看看下面的代码片段。
tsexport 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,会发生什么呢?
tsexport type BasicPrimitive = number | string | boolean;export function doStuff(value: BasicPrimitive) {if (Math.random() < 0.5) {return undefined;}return value;}
我们可以查看 TypeScript 4.1 Playground 中的结果。虽然我们可能希望 TypeScript 将 doStuff 的返回类型显示为 BasicPrimitive | undefined,但它却显示为 string | number | boolean | undefined!这是怎么回事?
这与 TypeScript 在内部表示类型的方式有关。当从一个或多个联合类型创建联合类型时,它总是会将这些类型标准化为一个新的扁平化联合类型——但这样做会丢失信息。类型检查器必须找到 string | number | boolean | undefined 中类型的每一种组合,以查看可以使用哪些类型别名,即使如此,对于 string | number | boolean 也可能存在多个类型别名。
在 TypeScript 4.2 中,我们的内部实现变得更加智能。我们通过保留原始代码的编写和构建方式的相关部分,来跟踪类型的构建过程。我们还会跟踪并区分其他别名实例的类型别名!
能够根据你在代码中的使用方式来回显类型,意味着作为 TypeScript 用户,你可以避免显示一些极其冗长的类型,这通常可以转化为更好的 .d.ts 文件输出、错误消息,以及编辑器中简要信息和签名帮助的类型展示。这可以使 TypeScript 对新手来说更加亲切。
欲了解更多信息,请查看第一个改进联合类型别名保留情况的拉取请求,以及第二个保留间接别名的拉取请求。
元组类型中的前置/中间剩余元素
在 TypeScript 中,元组类型旨在模拟具有特定长度和元素类型的数组。
ts// A tuple that stores a pair of numberslet a: [number, number] = [1, 2];// A tuple that stores a string, a number, and a booleanlet b: [string, number, boolean] = ["hello", 42, true];
随着时间的推移,TypeScript 的元组类型变得越来越复杂,因为它们也被用来模拟 JavaScript 中的参数列表等内容。因此,它们可以拥有可选元素和剩余元素,甚至可以为工具和可读性添加标签。
tsTry// A tuple that has either one or two strings.letc : [string, string?] = ["hello"];c = ["hello", "world"];// A labeled tuple that has either one or two strings.letd : [first : string,second ?: string] = ["hello"];d = ["hello", "world"];// A tuple with a *rest element* - holds at least 2 strings at the front,// and any number of booleans at the back.lete : [string, string, ...boolean[]];e = ["hello", "world"];e = ["hello", "world", false];e = ["hello", "world", true, false, true];
在 TypeScript 4.2 中,剩余元素的使用方式得到了专门扩展。在以前的版本中,TypeScript 仅允许在元组类型的最后位置使用 ...rest 元素。
然而,现在剩余元素可以出现在元组内的任何位置——仅有极少数限制。
tsTryletfoo : [...string[], number];foo = [123];foo = ["hello", 123];foo = ["hello!", "hello!", "hello!", 123];letbar : [boolean, ...string[], boolean];bar = [true, false];bar = [true, "some text", false];bar = [true, "some", "separated", "text", false];
唯一的限制是,只要剩余元素后面没有其他可选元素或剩余元素,它就可以放置在元组中的任何位置。换句话说,每个元组只能有一个剩余元素,且剩余元素之后不能有可选元素。
tsTryinterfaceClown {/*...*/}interfaceJoker {/*...*/}letA rest element cannot follow another rest element.1265A rest element cannot follow another rest element.StealersWheel : [...Clown [], "me", ...Joker []];letAn optional element cannot follow a rest element.1266An optional element cannot follow a rest element.StringsAndMaybeBoolean : [...string[], boolean?];
这些非尾部的剩余元素可用于模拟那些接受任意数量前导参数,随后跟有几个固定参数的函数。
tsTrydeclare functiondoStuff (...args : [...names : string[],shouldCapitalize : boolean]): void;doStuff (/*shouldCapitalize:*/ false)doStuff ("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);
尽管 JavaScript 没有用于模拟前置剩余参数的语法,但我们仍然能够通过声明 ...args 剩余参数(其使用带有前置剩余元素的元组类型)来将 doStuff 声明为接受前置参数的函数。这有助于对现有的许多 JavaScript 代码进行建模!
有关更多详情,请参阅原始拉取请求。
对 in 运算符进行更严格的检查
在 JavaScript 中,在 in 运算符的右侧使用非对象类型会导致运行时错误。TypeScript 4.2 确保可以在设计时捕获此问题。
tsTry"foo" inType 'number' is not assignable to type 'object'.2322Type 'number' is not assignable to type 'object'.42 ;
在大多数情况下,此检查相当保守,因此如果你收到关于此的错误,很可能是代码本身存在问题。
非常感谢外部贡献者 Jonas Hübotter 提交的拉取请求!
--noPropertyAccessFromIndexSignature
TypeScript 最初引入索引签名时,你只能通过“方括号”元素访问语法(如 person["name"])来获取它们声明的属性。
tsTryinterfaceSomeType {/** This is an index signature. */[propName : string]: any;}functiondoStuff (value :SomeType ) {letx =value ["someProperty"];}
在处理具有任意属性的对象时,这显得非常繁琐。例如,想象一个 API,其中由于在末尾多加了一个 s 字符而拼错属性名是很常见的。
tsTryinterfaceOptions {/** File patterns to be excluded. */exclude ?: string[];/*** It handles any extra properties that we haven't declared as type 'any'.*/[x : string]: any;}functionprocessOptions (opts :Options ) {// Notice we're *intentionally* accessing `excludes`, not `exclude`if (opts .excludes ) {console .error ("The option `excludes` is not valid. Did you mean `exclude`?");}}
为了简化这类情况,前段时间,TypeScript 允许在类型具有字符串索引签名时使用“点号”属性访问语法(如 person.name)。这也使得将现有的 JavaScript 代码转换为 TypeScript 变得更加容易。
然而,放宽此限制也意味着更容易拼错显式声明的属性。
tsTryfunctionprocessOptions (opts :Options ) {// ...// Notice we're *accidentally* accessing `excludes` this time.// Oops! Totally valid.for (constexcludePattern ofopts .excludes ) {// ...}}
在某些情况下,用户更希望显式地选择使用索引签名——他们希望在点号属性访问与特定属性声明不对应时收到错误消息。
这就是为什么 TypeScript 引入了一个名为 noPropertyAccessFromIndexSignature 的新标志。在此模式下,你将选择使用 TypeScript 旧有的报错行为。此新设置不属于 strict 标志家族,因为我们认为用户会发现它在某些代码库中比在其他代码库中更有用。
你可以通过阅读相应的拉取请求来更详细地了解此功能。我们还要特别感谢 Wenlu Wang 提交此拉取请求!
abstract 构造签名
TypeScript 允许我们将类标记为 abstract(抽象类)。这告诉 TypeScript 该类仅用于被继承,且某些成员需要由任何子类填充才能实际创建实例。
tsTryabstract classShape {abstractgetArea (): number;}newCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.Shape ();classSquare extendsShape {#sideLength: number;constructor(sideLength : number) {super();this.#sideLength =sideLength ;}getArea () {return this.#sideLength ** 2;}}// Works fine.newSquare (42);
为了确保在 new 操作符中使用 abstract 类的限制得到一致应用,你不能将 abstract 类赋值给任何期望构造签名的内容。
tsTryinterfaceHasArea {getArea (): number;}letType 'typeof Shape' is not assignable to type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.2322Type 'typeof Shape' is not assignable to type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.: new () => Ctor HasArea =Shape ;
这在如果我们打算运行 new Ctor 这样的代码时是正确的,但在我们想要编写 Ctor 的子类时,它就过于严格了。
tsTryabstract classShape {abstractgetArea (): number;}interfaceHasArea {getArea (): number;}functionmakeSubclassWithArea (Ctor : new () =>HasArea ) {return class extendsCtor {getArea () {return 42}};}letArgument of type 'typeof Shape' is not assignable to parameter of type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Shape' is not assignable to parameter of type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.MyShape =makeSubclassWithArea (); Shape
它也无法与 InstanceType 等内置辅助类型很好地配合工作。
tsTrytypeMyInstance =InstanceType <typeofShape >;
这就是为什么 TypeScript 4.2 允许你在构造签名上指定 abstract 修饰符。
tsTryinterfaceHasArea {getArea (): number;}// Works!letCtor : abstract new () =>HasArea =Shape ;
在构造签名中添加 abstract 修饰符表示你可以传入 abstract 构造函数。它并不会阻止你传入其他“具体”的类/构造函数——它实际上只是表示没有直接运行构造函数的意图,因此传入任一类类型都是安全的。
此功能允许我们以支持抽象类的方式编写 mixin 工厂。例如,在下面的代码片段中,我们能够将 mixin 函数 withStyles 与 abstract 类 SuperClass 一起使用。
tsTryabstract classSuperClass {abstractsomeMethod (): void;badda () {}}typeAbstractConstructor <T > = abstract new (...args : any[]) =>T functionwithStyles <T extendsAbstractConstructor <object>>(Ctor :T ) {abstract classStyledClass extendsCtor {getStyles () {// ...}}returnStyledClass ;}classSubClass extendswithStyles (SuperClass ) {someMethod () {this.someMethod ()}}
请注意,withStyles 演示了一条特定规则:一个扩展了通用且有抽象构造函数约束(如 Ctor)的值的类(如 StyledClass)也必须声明为 abstract。这是因为无法知道是否传入了具有更多抽象成员的类,因此无法确定子类是否实现了所有抽象成员。
你可以阅读其拉取请求以了解更多关于抽象构造签名的信息。
使用 --explainFiles 理解项目结构
对于 TypeScript 用户来说,一个非常常见的场景是问“为什么 TypeScript 包含这个文件?”。推断程序的源文件是一个复杂的过程,因此有很多原因会导致特定的 lib.d.ts 组合被使用,某些 node_modules 中的文件被包含,以及某些文件即使我们以为通过指定 exclude 会将它们排除在外,它们却依然被包含进来。
这就是为什么 TypeScript 现在提供了一个 explainFiles 标志。
shtsc --explainFiles
使用此选项时,TypeScript 编译器将输出非常冗长的信息,解释某个文件最终被包含在你的程序中的原因。为了更轻松地阅读,你可以将输出重定向到一个文件,或者通过管道传给一个易于查看的程序。
sh# Forward output to a text filetsc --explainFiles > explanation.txt# Pipe output to a utility program like `less`, or an editor like VS Codetsc --explainFiles | lesstsc --explainFiles | code -
通常,输出会首先列出包含 lib.d.ts 文件的原因,然后是本地文件,最后是 node_modules 文件。
TS_Compiler_Directory/4.2.2/lib/lib.es5.d.tsLibrary referenced via 'es5' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.tsLibrary referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.tsLibrary referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.tsLibrary referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.tsLibrary referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.tsLibrary referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.tsLibrary referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.tsLibrary 'lib.esnext.d.ts' specified in compilerOptions... More Library References...foo.tsMatched by include pattern '**/*' in 'tsconfig.json'
目前,我们不对输出格式做出任何保证——它可能会随时间而变化。关于这一点,如果你有任何建议,我们很乐意改进此格式!
欲了解更多信息,请查看原始拉取请求!
改进逻辑表达式中对未调用函数的检查
得益于 Alex Tarasyuk 的进一步改进,TypeScript 的未调用函数检查现在应用于 && 和 || 表达式中。
在 strictNullChecks 下,以下代码现在会报错。
tsfunction shouldDisplayElement(element: Element) {// ...return true;}function getVisibleItems(elements: Element[]) {return elements.filter((e) => shouldDisplayElement && e.children.length);// ~~~~~~~~~~~~~~~~~~~~// This condition will always return true since the function is always defined.// Did you mean to call it instead.}
有关更多详情,请查看此处的拉取请求。
解构变量可以显式标记为未使用
得益于 Alex Tarasyuk 的另一个拉取请求,你现在可以通过在变量名前加上下划线(_ 字符)来将解构变量标记为未使用。
tslet [_first, second] = getValues();
以前,如果 _first 后面从未被使用,TypeScript 会在 noUnusedLocals 下报错。现在,TypeScript 会识别出 _first 是有意以该下划线命名,因为它本就没有被使用的意图。
有关更多详情,请查看完整更改。
放宽可选属性与字符串索引签名之间的规则
字符串索引签名是一种对字典类对象进行类型化的方法,你希望允许使用任意键进行访问。
tsTryconstmovieWatchCount : { [key : string]: number } = {};functionwatchMovie (title : string) {movieWatchCount [title ] = (movieWatchCount [title ] ?? 0) + 1;}
当然,对于任何尚未存在于字典中的电影标题,movieWatchCount[title] 将是 undefined(TypeScript 4.1 添加了选项 noUncheckedIndexedAccess,以在读取此类索引签名时包含 undefined)。尽管显而易见会有某些字符串不存在于 movieWatchCount 中,但由于 undefined 的存在,以前版本的 TypeScript 将可选对象属性视为不可分配给否则兼容的索引签名。
tsTrytypeWesAndersonWatchCount = {"Fantastic Mr. Fox"?: number;"The Royal Tenenbaums"?: number;"Moonrise Kingdom"?: number;"The Grand Budapest Hotel"?: number;};declare constwesAndersonWatchCount :WesAndersonWatchCount ;constmovieWatchCount : { [key : string]: number } =wesAndersonWatchCount ;// ~~~~~~~~~~~~~~~ error!// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.// Property '"Fantastic Mr. Fox"' is incompatible with index signature.// Type 'number | undefined' is not assignable to type 'number'.// Type 'undefined' is not assignable to type 'number'. (2322)
TypeScript 4.2 允许了这种赋值。但是,它不允许赋值包含 undefined 的非可选属性,也不允许将 undefined 写入特定键。
tsTrytypeBatmanWatchCount = {"Batman Begins": number | undefined;"The Dark Knight": number | undefined;"The Dark Knight Rises": number | undefined;};declare constbatmanWatchCount :BatmanWatchCount ;// Still an error in TypeScript 4.2.constType 'BatmanWatchCount' is not assignable to type '{ [key: string]: number; }'. Property '"Batman Begins"' is incompatible with index signature. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.2322Type 'BatmanWatchCount' is not assignable to type '{ [key: string]: number; }'. Property '"Batman Begins"' is incompatible with index signature. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.: { [ movieWatchCount key : string]: number } =batmanWatchCount ;// Still an error in TypeScript 4.2.// Index signatures don't implicitly allow explicit `undefined`.Type 'undefined' is not assignable to type 'number'.2322Type 'undefined' is not assignable to type 'number'.movieWatchCount ["It's the Great Pumpkin, Charlie Brown"] =undefined ;
新规则也不适用于数字索引签名,因为它们被假定为类似数组且密集的。
tsTrydeclare letsortOfArrayish : { [key : number]: string };declare letnumberKeys : { 42?: string };Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'. Property '42' is incompatible with index signature. Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.2322Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'. Property '42' is incompatible with index signature. Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.= sortOfArrayish numberKeys ;
你可以通过阅读原始 PR 来更好地理解此更改。
声明缺失的辅助函数
得益于来自 Alexander Tarasyuk 的社区拉取请求,我们现在有了一个基于调用点声明新函数和方法的快速修复!

破坏性变更
我们始终努力最大限度地减少发布中的破坏性更改。TypeScript 4.2 包含一些破坏性更改,但我们相信在升级过程中应该是可控的。
lib.d.ts 更新
与每个 TypeScript 版本一样,lib.d.ts 的声明(特别是为 Web 环境生成的声明)发生了变化。虽然更改多种多样,但 Intl 和 ResizeObserver 的声明可能是最具破坏性的。
noImplicitAny 错误应用于松散的 yield 表达式
当 yield 表达式的值被捕获,但 TypeScript 无法立即确定你打算让它接收什么类型(即 yield 表达式不是上下文类型的)时,TypeScript 现在会发出隐式 any 错误。
tsTryfunction*g1 () {const'yield' expression implicitly results in an 'any' type because its containing generator lacks a return-type annotation.7057'yield' expression implicitly results in an 'any' type because its containing generator lacks a return-type annotation.value =yield 1;}function*g2 () {// No error.// The result of `yield 1` is unused.yield 1;}function*g3 () {// No error.// `yield 1` is contextually typed by 'string'.constvalue : string = yield 1;}function*g4 ():Generator <number, void, string> {// No error.// TypeScript can figure out the type of `yield 1`// from the explicit return type of `g4`.constvalue = yield 1;}
在相应的更改中查看更多详情。
扩展的未调用函数检查
如上所述,使用 strictNullChecks 时,未调用函数检查现在将一致地运行在 && 和 || 表达式中。这可能是一个新的破坏性来源,但通常是现有代码中逻辑错误的征兆。
JavaScript 中的类型参数不再被解析为类型参数
类型参数在 JavaScript 中本就不被允许,但在 TypeScript 4.2 中,解析器将以更符合规范的方式解析它们。因此,当在 JavaScript 文件中编写以下代码时:
tsf<T>(100);
TypeScript 将其解析为以下 JavaScript:
jsf < T > 100;
如果你一直在利用 TypeScript 的 API 在 JavaScript 文件中解析类型结构(这在尝试解析 Flow 文件时可能发生),这可能会影响你。
参见拉取请求,获取有关检查内容的更多详情。
扩展运算符的元组大小限制
元组类型可以通过在 TypeScript 中使用任何形式的展开语法(...)来创建。
ts// Tuple types with spread elementstype NumStr = [number, string];type NumStrNumStr = [...NumStr, ...NumStr];// Array spread expressionsconst numStr = [123, "hello"] as const;const numStrNumStr = [...numStr, ...numStr] as const;
有时这些元组类型可能会意外变得巨大,这会导致类型检查耗费很长时间。为了防止类型检查过程挂起(这在编辑器场景中尤其糟糕),TypeScript 引入了一个限制器以避免进行所有这些工作。
你可以查看此拉取请求以了解更多详情。
.d.ts 扩展名不能用于导入路径
在 TypeScript 4.2 中,导入路径中包含 .d.ts 扩展名现在会报错。
ts// must be changed to something like// - "./foo"// - "./foo.js"import { Foo } from "./foo.d.ts";
相反,你的导入路径应反映你的加载器在运行时将执行的操作。你可以改用以下任一导入方式。
tsimport { Foo } from "./foo";import { Foo } from "./foo.js";import { Foo } from "./foo/index.js";
回退模板字面量推断
此更改移除了 TypeScript 4.2 Beta 版本中的一个功能。如果你尚未升级到我们上一个稳定版本之后,你将不会受到影响,但你可能仍对这次更改感兴趣。
TypeScript 4.2 的测试版包含对模板字符串推断的更改。在此更改中,模板字符串字面量要么被赋予模板字符串类型,要么简化为多个字符串字面量类型。这些类型在赋值给可变变量时会加宽为 string。
tsdeclare const yourName: string;// 'bar' is constant.// It has type '`hello ${string}`'.const bar = `hello ${yourName}`;// 'baz' is mutable.// It has type 'string'.let baz = `hello ${yourName}`;
这与字符串字面量推断的工作方式类似。
ts// 'bar' has type '"hello"'.const bar = "hello";// 'baz' has type 'string'.let baz = "hello";
因此,我们认为使模板字符串表达式具有模板字符串类型将是“一致的”;然而,从我们所见所闻来看,这并不总是可取的。
作为回应,我们撤销了此功能(以及潜在的破坏性更改)。如果你确实希望模板字符串表达式被赋予类似于字面量的类型,你总是可以在其末尾添加 as const。
tsdeclare const yourName: string;// 'bar' has type '`hello ${string}`'.const bar = `hello ${yourName}` as const;// ^^^^^^^^// 'baz' has type 'string'.const baz = `hello ${yourName}`;
TypeScript 中 visitNode 的 lift 回调使用了不同的类型
TypeScript 有一个接受 lift 函数的 visitNode 函数。lift 现在期望一个 readonly Node[] 而不是 NodeArray<Node>。这在技术上是一个 API 破坏性更改,你可以在此处阅读更多信息。