作者 | Daniel
译者 | 张健欣
策划 | 李俊辰
今天,我们很高兴地宣布 TypeScript 4.2 的发布!
TypeScript 是 JavaScript 的一个扩展,增加了静态类型和类型检查。使用类型,你可以准确声明你的函数接收什么类型参数,返回什么类型结果。然后,你可以使用 TypeScript 类型检查器来捕获许多常见错误,例如拼写错误、忘记处理null
和undefined
等等。因为 TypeScript 代码看起来就像带类型的 JavaScript,所以你所知的关于 JavaScript 的所有东西仍然适用。当你需要的时候,你的类型可以被剥离出来,留下干净的、可读的、可运行的 JavaScript,可以在任何地方运行。你可以 访问我们的网站,了解更多关于 TypeScript 的信息。
开始使用 TypeScript 4.2,你可以 通过 NuGet 获取它,或者使用如下 npm 命令:
npm install typescript
让我们来看看 TypeScript 4.2 有哪些功能!
in
运算符的更严格的检查--noPropertyAccessFromIndexSignature
abstract
构造符号--explainFiles
标记更智能的类型别名保留
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 playground 中 会发生什么。虽然我们可能希望 TypeScript 显示doStuff
的类型为BasicPrimitive | undefined
,但它实际显示的是string | number | boolean | undefined
!为什么会这样?这与 TypeScript 如何在内部表示类型有关。当用一个或多个组合类型创建组合类型时,它总是将这些类型规范化为一个扁平的组合类型——但这样做会丢失信息。类型检查器必须从string | number | boolean | undefined
的所有组合类型中来看看哪种类型别名被使用了,即使这样,string | number | boolean
还可能有多个类型别名。
在 TypeScript 4.2 中,我们的内部结构更加智能。我们通过保留类型各部分最初是如何被编写和构建的,来跟踪类型是如何被构建的。我们还对类型别名与其它别名实例进行跟踪和区分!
能够根据你在代码中使用它们的方式来打印类型,意味着作为一名 TypeScript 用户,你可以避免显示一些非常庞大的类型,这通常会转化为更好的.d.ts
文件输出、异常信息和编辑器中的快速信息和符号帮助中的类型显示。这有助于让新手更容易上手 TypeScript。
有关更多信息,请查看第一个拉取请求,它改进了有关保留组合类型别名的各种用例,以及第二个拉取请求,它保留了间接别名。
https://github.com/microsoft/TypeScript/pull/42149
https://github.com/microsoft/TypeScript/pull/42284
元组类型中的前导 / 中间剩余元素
在 TypeScript 中,元组类型用于对具有特定长度和元素类型的数组进行建模。
// A tuple that stores a pair of numbers
let a: [number, number] = [1, 2];
// A tuple that stores a string, a number, and a boolean
let b: [string, number, boolean] = ["hello", 42, true];
随着时间的推移,TypeScript 的元组类型变得越来越复杂,因为它们也被用于 JavaScript 中的参数列表之类的建模。因此,它们可以有可选元素和剩余元素,甚至可以有用于工具和可读性的标签。
// A tuple that has either one or two strings.
let c: [string, string?] = ["hello"];
c = ["hello", "world"];
// A labeled tuple that has either one or two strings.
let d: [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.
let e: [string, string, ...boolean[]];
e = ["hello", "world"];
e = ["hello", "world", false];
e = ["hello", "world", true, false, true];
在 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];
唯一的限制是剩余元素可以放在一个元组的任何位置,只要它后面没有另一个可选元素或剩余元素。换句话说,每个元组只有一个剩余元素,并且剩余元素后面不能有可选元素。
interface Clown { /*...*/ }
interface Joker { /*...*/ }
let StealersWheel: [...Clown[], "me", ...Joker[]];
// ~~~~~~~~~~ Error!
// A rest element cannot follow another rest element.
let StringsAndMaybeBoolean: [...string[], boolean?];
// ~~~~~~~~ Error!
// An optional element cannot follow a rest element.
这些没有后缀的剩余元素可以被用来对采用任意数量的前导参数(后面跟几个固定参数)的函数进行建模。
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;
doStuff(/*shouldCapitalize:*/ false)
doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);
尽管 JavaScript 没有任何语法来为前导剩余参数建模,我们仍然可以通过使用一个带前导剩余元素的元组类型来声明...args
剩余参数,来将doStuff
声明为一个接收前导参数的函数。这有助于对大量现有的 JavaScript 进行建模!
有关更多详细信息,请查看原始的拉取请求:
https://github.com/microsoft/TypeScript/pull/41544
针对in
操作符的更严格的检查
在 JavaScript 中,在in
操作符右侧使用一个非对象类型是一个运行时错误。TypeScript 4.2 确保这可以在设计时发现这个错误。
"foo" in 42
// ~~
// error! The right-hand side of an 'in' expression must not be a primitive.
这个检查在很大程度上是相当保守的,因此如果你收到了这个错误,那么代码中很可能有问题。非常感谢我们的外部贡献者 Jonas Hübotter 提交的 拉取请求!
引入新标志
当 TypeScript 第一次引入索引符号时,你只能使用“方括号包括的”元素获取语法(如person["name"]
)来获取它们声明的属性。
interface SomeType {
/** This is an index signature. */
[propName: string]: any;
}
function doStuff(value: SomeType) {
let x = value["someProperty"];
}
在我们需要处理具有任意属性的对象的情况下,这会变得很麻烦。例如,假设一个 API,在一个属性名末尾多打了一个s
字符是很常见的拼写错误。
interface Options {
/** File patterns to be excluded. */
exclude?: string[];
/**
* It handles any extra properties that we haven't declared as type 'any'.
*/
[x: string]: any;
}
function processOptions(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 变得更容易。
然而,放松限制也意味着错误拼写一个显式声明的属性变得容易得多。
function processOptions(opts: Options) {
// ...
// Notice we're *accidentally* accessing `excludes` this time.
// Oops! Totally valid.
for (const excludePattern of opts.excludes) {
// ...
}
}
在某些情况下,用户更愿意显式地选择索引符号——当点式属性访问与特定属性声明不对应时,他们更愿意收到错误消息。
这就是为什么 TypeScript 引入了一个新的标志,--noPropertyAccessFromIndexSignature
。在这种模式中,你将选择使用 TypeScript 的旧行为来发出错误。这个新的设置并不在strict
标志家族中,因为我们相信用户会发现它在特定代码库上比在其它代码库上更有用。
你可以通过阅读相应的拉取请求,来了解这个功能的更多细节:
https://github.com/microsoft/TypeScript/pull/40171/
我们也要向给我们发送这个拉取请求的 Wenlu Wang 致以衷心的感谢!
abstract
构造符号
TypeScript 允许我们将一个类标记为 abstract。这告诉 TypeScript,这个类只会被继承,特别成员需要由任何实际创建的子类示例填充。
abstract class Shape {
abstract getArea(): number;
}
// Error! Can't instantiate an abstract class.
new Shape();
class Square extends Shape {
#sideLength: number;
constructor(sideLength: number) {
this.#sideLength = sideLength;
}
getArea() {
return this.#sideLength ** 2;
}
}
// Works fine.
new Square(42);
为了确保在新建abstract
类时始终应用此限制,你不能将abstract
类分配给任何需要构造符号的对象。
interface HasArea {
getArea(): number;
}
// Error! Cannot assign an abstract constructor type to a non-abstract constructor type.
let Ctor: new () => HasArea = Shape;
如果我们打算像new Ctor
那样运行代码,那么这样做是正确的,但是如果我们想要写一个Ctor
的子类,那么这样做就过于严格了。
functon makeSubclassWithArea(Ctor: new () => HasArea) {
return class extends Ctor {
getArea() {
// ...
}
}
}
let MyShape = makeSubclassWithArea(Shape);
对于InstanceType
这样的内置助手类型,它也不能很好地生效。
// Error!
// Type 'typeof Shape' does not satisfy the constraint 'new (...args: any) => any'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.
type MyInstance = InstanceType<typeof Shape>;
这就是 TypeScript 4.2 允许你在构造符号上指定一个abstract
修饰符的原因。
interface HasArea {
getArea(): number;
}
// Works!
let Ctor: abstract new () => HasArea = Shape;
// ^^^^^^^^
将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()
}
}
请注意,withStyles
演示的是一个特定的规则,其中类(例如StyledClass
)继承了一个泛型值,并被一个抽象构造器(例如Ctor
)限定,必须被声明为abstract
。这是因为无法知道是否传入了具有更多抽象成员的类,因此不可能知道子类是否实现了所有的抽象成员。
你可以通过这个拉取请求阅读更多关于抽象构造符号的信息:
https://github.com/microsoft/TypeScript/pull/36392
利用--explainFiles
理解你的项目结构
对于 TypeScript 用户来说,一个出人意料的常见场景是问“为什么 TypeScript 包含这个文件?”。推断程序的文件是一个复杂的过程,因此有很多原因可以解释为什么要使用lib.d.ts
的特定组合,为什么要包括node_modules
中的某些文件,以及要包含某些文件金骨干我们认为指定exclude
会将它们排除在外。
这就是 TypeScript 现在 提供explainFiles
标志的原因。
tsc --explainFiles
当使用此选项时,TypeScript 编译器将给出一些非常详细的输出,说明文件为什么会出现在程序中。为了更容易读取,你可以将输出转到一个文件,或者通过管道将其传输到一个可以轻松查看它的程序。
# Forward output to a text file
tsc --explainFiles > expanation.txt
# Pipe output to a utility program like `less`, or an editor like VS Code
tsc --explainFiles | less
tsc --explainFiles | code -
通常,输出将首先列出包含lib.d.ts
文件的原因,然后列出本地文件的原因,然后列出node_modules
文件的原因。
TS_Compiler_Directory/4.2.2/lib/lib.es5.d.ts
Library 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.ts
Library 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.ts
Library 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.ts
Library 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.ts
Library 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.ts
Library 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.ts
Library 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.ts
Library 'lib.esnext.d.ts' specified in compilerOptions
... More Library References...
foo.ts
Matched by include pattern '**/*' in 'tsconfig.json'
现在,我们对输出格式没有任何保证——它可能会随着时间变化。在这一点上,如果你有任何建议,我们有兴趣改进这种格式!有关更多信息,请查看原始的拉取请求:
https://github.com/microsoft/TypeScript/pull/40011
逻辑表达式中改进的未调用函数检查
由于 Alex Tarasyuk 的进一步改进,TypeScript 的未调用函数检查现在可以用于&&
和||
表达式。在--strictNullChecks
模式下,下列代码现在会报错。
function 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.
}
获取更多细节,请查看这里的拉取请求:
https://github.com/microsoft/TypeScript/issues/40197
解构变量可以显式标记为未使用
由于 Alex Tarasyuk 的另一个拉取请求,你现在可以通过在解构变量前增加一个下划线(_
字符),来将解构变量标记为未使用。
let [_first, second] = getValues();
以前,如果_first
以后从未使用过,TypeScript 会报一个noUnusedLocals
错误。现在,TypeScript 将意识到,_first
是故意用下划线命名的,因为没有使用它的意图。
获取更多细节,请查看完整的更改:
https://github.com/microsoft/TypeScript/pull/41378
可选属性和字符串索引符号之间的宽松规则
字符串索引符号一种类似字典的对象,你可以使用其中的任意键进行访问:
const movieWatchCount: { [key: string]: number } = {};
function watchMovie(title: string) {
movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}
当然,对于字典中还没有任何电影标题,movieWatchCount[title]
会报undefined
(TypeScript 4.1 增加了--noUncheckedIndexedAccess
选项,以便在读取这样的索引符号时包括undefined
)。尽管很明显movieWatchCount
中肯定有一些字符串不存在,但是由于undefined
的存在,TypeScript 的早期版本认为对象的可选属性不能用兼容索引符号赋值。
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;
// ~~~~~~~~~~~~~~~ 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
写到特定键:
type BatmanWatchCount = {
"Batman Begins": number | undefined;
"The Dark Knight": number | undefined;
"The Dark Knight Rises": number | undefined;
};
declare const batmanWatchCount: BatmanWatchCount;
// Still an error in TypeScript 4.2.
// `undefined` is only ignored when properties are marked optional.
const movieWatchCount: { [key: string]: number } = batmanWatchCount;
// Still an error in TypeScript 4.2.
// Index signatures don't implicitly allow explicit `undefined`.
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;
新规则也不适用于数字索引符号,因为它们被假定为类似数组且密集的:
declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };
// Error! Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'.
sortOfArrayish = numberKeys;
你可以通过阅读原始拉取请求来更好地了解这个变化:
https://github.com/microsoft/TypeScript/pull/41921
声明缺失的帮助函数
由于来自 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;
// ~~~~~~~
// Error!
// 'yield' expression implicitly results in an 'any' type
// because its containing generator lacks a return-type annotation.
}
function* g2() {
// No error.
// The result of `yield 1` is unused.
yield 1;
}
function* g3() {
// No error.
// `yield 1` is contextually typed by 'string'.
const value: string = yield 1;
}
function* g3(): Generator<number, void, string> {
// No error.
// TypeScript can figure out the type of `yield 1`
// from the explicit return type of `g3`.
const value = yield 1;
}
请参见相应更改中的更多细节:
https://github.com/microsoft/TypeScript/pull/41348
扩展的未调用的函数检查
如上所述,在使用--strictNullChecks
时,未调用的函数检查现在将在&&
和||
表达式中一致地操作。这可能是新中断的来源,但通常表示现有代码中存在逻辑错误。
JavaScript 中的类型参数不被解析为类型参数
JavaScript 中已经不允许使用类型参数,但是在 TypeScript 4.2 中,解析器将以更符合规范的形式解析它们。因此,在 JavaScript 文件中写如下代码时:
f<T>(100)
TypeScript 会将它解析为如下 JavaScript:
(f < T) > (100)
如果你正利用 TypeScript 的 API 来解析 JavaScript 文件中的类型构造(在尝试解析 Flow 文件时会发生),这可能会对你有所影响。
in
运算符不在允许在后边出现原始类型
如前所述,在in
运算符右边使用原始类型是一个错误,而 TypeScript 4.2 对这类代码更严格。
"foo" in 42
// ~~
// error! The right-hand side of an 'in' expression must not be a primitive.
有关检查内容的更多细节请查看拉取请求:
https://github.com/microsoft/TypeScript/pull/41928
spreads 的元组大小限制
元组类型可以通过在 TypeScript 中使用任何类型的 spread 语法(...
)来生成。
// Tuple types with spread elements
type NumStr = [number, string];
type NumStrNumStr = [...NumStr, ...NumStr];
// Array spread expressions
const numStr = [123, "hello"] as const;
const numStrNumStr = [...numStr, ...numStr] as const;
有时,这些元素类型可能会意外地增长到非常大,这会导致类型检查花费很长时间。TypeScript 没有让类型检查进程挂起(在编辑器场景中尤其糟糕),而是设置了一个限制器来避免执行所有这些检查。
你可以查看这个拉取请求来获取更多细节:
https://github.com/microsoft/TypeScript/pull/42448
.d.ts
扩展不能用于导入路径
在 TypeScript 4.2 中,导入路径的扩展名中包含.d.ts
现在是一个错误。
// must be changed something like
// - "./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' is constant.
// It has type '`hello ${string}`'.
const bar = `hello ${yourName}`;
// 'baz' is mutable.
// It has type 'string'.
let baz = `hello ${yourName}`;
这和字符串字面推断的工作方式类似。
// 'bar' has type '"hello"'.
const bar = "hello";
// 'baz' has type 'string'.
let baz = "hello";
因此,我们认为使模板字符串表达式具有模板字符串类型是“一致的”;然而,从我们的所见所闻来看,这并不总是可取的。作为回应,我们恢复了这个特性(以及潜在的破坏性变更)。如果你确实希望给模板字符串表达式指定类似字面的类型,你可以在其末尾添加as const
。
declare const yourName: string;
// 'bar' has type '`hello ${string}`'.
const bar = `hello ${yourName}` as const;
// ^^^^^^^^
// 'baz' has type 'string'.
const baz = `hello ${yourName}`;
visitNode
中 TypeScript 的lift
回调使用不同的类型
TypeScript 有一个visitNode
函数,接收lift
函数。lift
现在需要一个readonly Node[]
而不是一个NodeArray<Node>
。这在技术上是一个破坏性变化,你可以通过下方链接了解更多信息:
https://github.com/microsoft/TypeScript/pull/42000
下一步?
虽然 4.2 刚刚发布,我们的团队已经在努力开发 TypeScript 4.3。你可以查看 TypeScript 4.3 迭代计划:
https://github.com/microsoft/TypeScript/issues/42762
和我们的滚动特性路线图:
https://github.com/Microsoft/TypeScript/wiki/Roadmap
你还可以使用 TypeScript 每日发行版(nightly release)以及我们的 nightly Visual Studio 代码扩展来保持最新。每日发行版往往是相当稳定的,早期反馈是被鼓励和赞赏的!
但是我们希望大部分用户现在使用 TypeScript 4.2;因此,如果你正在使用 TypeScript 4.2,我们希望这个版本容易上手,并使你的工作效率更高。如果没有,我们希望听到你的事例!我们想要保证 TypeScript 在编码中给您带来了乐趣,我希望我们已经做到了这一点。
编码快乐!
– Daniel Rosenwasser 和 TypeScript 团队
作者介绍
Daniel 技术经理,专注于 TypeScript。