TypeScript 4.0

可变元组类型

¥Variadic Tuple Types

考虑 JavaScript 中一个名为 concat 的函数,它接受两个数组或元组类型并将它们连接在一起以创建一个新数组。

¥Consider a function in JavaScript called concat that takes two array or tuple types and concatenates them together to make a new array.

js
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}

另外,考虑一下 tail,它接受一个数组或元组作为参数,并返回除第一个元素之外的所有元素。

¥Also consider tail, that takes an array or tuple, and returns all elements but the first.

js
function tail(arg) {
const [_, ...result] = arg;
return result;
}

如何在 TypeScript 中为这两个函数编写类型?

¥How would we type either of these in TypeScript?

对于 concat,在旧版本语言中我们唯一能做的有效事情就是尝试编写一些重载。

¥For concat, the only valid thing we could do in older versions of the language was to try and write some overloads.

ts
function 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 有一个参数的情况添加一些代码。

¥Uh…okay, that’s…seven overloads for when the second array is always empty. Let’s add some for when arr2 has one argument.

ts
function 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 这样的函数时相同的问题。

¥We hope it’s clear that this is getting unreasonable. Unfortunately, you’d also end up with the same sorts of issues typing a function like tail.

这是我们喜欢称之为 “被一千个重载杀死” 的另一个例子,它甚至不能解决一般问题。它只为我们想要编写的重载提供正确的类型。如果我们想要实现一个包罗万象的案例,我们需要一个如下所示的重载:

¥This is another case of what we like to call “death by a thousand overloads”, and it doesn’t even solve the problem generally. It only gives correct types for as many overloads as we care to write. If we wanted to make a catch-all case, we’d need an overload like the following:

ts
function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;

但是,当使用元组时,该签名不会编码任何有关输入长度或元素顺序的信息。

¥But that signature doesn’t encode anything about the lengths of the input, or the order of the elements, when using tuples.

TypeScript 4.0 带来了两项根本性的变化以及推断改进,使这些类型识别成为可能。

¥TypeScript 4.0 brings two fundamental changes, along with inference improvements, to make typing these possible.

第一个变化是元组类型语法中的展开现在可以是泛型的了。这意味着即使我们不知道操作的实际类型,我们也可以对元组和数组进行高阶运算。在这些元组类型中,当泛型展开被实例化(或替换为实际类型)时,它们可以生成其他类型的数组和元组类型。

¥The first change is that spreads in tuple type syntax can now be generic. This means that we can represent higher-order operations on tuples and arrays even when we don’t know the actual types we’re operating over. When generic spreads are instantiated (or, replaced with a real type) in these tuple types, they can produce other sets of array and tuple types.

例如,这意味着我们可以像 tail 这样的函数类型,而不会出现 “被一千个重载杀死” 的问题。

¥For example, that means we can type function like tail, without our “death by a thousand overloads” issue.

ts
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
 
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
 
const r1 = tail(myTuple);
const r1: [2, 3, 4]
 
const r2 = tail([...myTuple, ...myArray] as const);
const r2: [2, 3, 4, ...string[]]
Try

第二个变化是剩余元素可以出现在元组中的任何位置。 - 不仅仅是在命令末尾!

¥The second change is that rest elements can occur anywhere in a tuple - not just at the end!

ts
type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

以前,TypeScript 会触发如下错误:

¥Previously, TypeScript would issue an error like the following:

A rest element must be last in a tuple type.

但在 TypeScript 4.0 中,此限制放宽了。

¥But with TypeScript 4.0, this restriction is relaxed.

请注意,当我们扩展一个长度未知的类型时,生成的类型也会变得无界,并且所有后续元素都会被计入生成的剩余元素类型。

¥Note that in cases when we spread in a type without a known length, the resulting type becomes unbounded as well, and all the following elements factor into the resulting rest element type.

ts
type Strings = [string, string];
type Numbers = number[];
type Unbounded = [...Strings, ...Numbers, boolean];

通过将这两种行为结合在一起,我们可以为 concat 编写一个类型良好的签名:

¥By combining both of these behaviors together, we can write a single well-typed signature for concat:

ts
type Arr = readonly any[];
 
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
Try

虽然该签名仍然有点长,但它只是一个不需要重复的签名,并且它为所有数组和元组提供了可预测的行为。

¥While that one signature is still a bit lengthy, it’s just one signature that doesn’t have to be repeated, and it gives predictable behavior on all arrays and tuples.

此功能本身就很棒,但在更复杂的场景中也能大放异彩。例如,考虑一个名为 partialCall部分应用参数 函数。partialCall 接受一个函数 - 我们称之为 f - 以及 f 所需的初始几个参数。然后它返回一个新函数,该函数接受 f 仍然需要的任何其他参数,并在收到这些参数时调用 f

¥This functionality on its own is great, but it shines in more sophisticated scenarios too. For example, consider a function to partially apply arguments called partialCall. partialCall takes a function - let’s call it f - along with the initial few arguments that f expects. It then returns a new function that takes any other arguments that f still needs, and calls f when it receives them.

js
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs);
}

TypeScript 4.0 改进了剩余参数和剩余元组元素的推断过程,以便我们可以对其进行类型识别并使其具有 “正常工作” 类型。

¥TypeScript 4.0 improves the inference process for rest parameters and rest tuple elements so that we can type this and have it “just work”.

ts
type Arr = readonly unknown[];
 
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}
Try

在这种情况下,partialCall 了解它最初可以接受和不可以接受的参数,并返回适当接受和拒绝任何剩余参数的函数。

¥In this case, partialCall understands which parameters it can and can’t initially take, and returns functions that appropriately accept and reject anything left over.

ts
const foo = (x: string, y: number, z: boolean) => {};
 
const f1 = partialCall(foo, 100);
Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.
 
const f2 = partialCall(foo, "hello", 100, true, "oops");
Expected 4 arguments, but got 5.2554Expected 4 arguments, but got 5.
 
// This works!
const f3 = partialCall(foo, "hello");
const f3: (y: number, z: boolean) => void
 
// What can we do with f3 now?
 
// Works!
f3(123, true);
 
f3();
Expected 2 arguments, but got 0.2554Expected 2 arguments, but got 0.
 
f3(123, "hello");
Argument of type 'string' is not assignable to parameter of type 'boolean'.2345Argument of type 'string' is not assignable to parameter of type 'boolean'.
Try

可变元组类型支持许多令人兴奋的新模式,尤其是在函数组合方面。我们预计可以利用它来更好地对 JavaScript 内置的 bind 方法进行类型检查。此外,还引入了一些其他推断改进和模式,如果你有兴趣了解更多信息,可以查看可变元组的 拉取请求

¥Variadic tuple types enable a lot of new exciting patterns, especially around function composition. We expect we may be able to leverage it to do a better job type-checking JavaScript’s built-in bind method. A handful of other inference improvements and patterns also went into this, and if you’re interested in learning more, you can take a look at the pull request for variadic tuples.

带标签的元组元素

¥Labeled Tuple Elements

改进元组类型和参数列表的体验非常重要,因为它使我们能够对常见的 JavaScript 习惯用法进行强类型验证。 - 实际上只是对参数列表进行切片和切块,然后将它们传递给其他函数。我们可以将元组类型用于剩余参数的想法正是这一点至关重要的地方。

¥Improving the experience around tuple types and parameter lists is important because it allows us to get strongly typed validation around common JavaScript idioms - really just slicing and dicing argument lists and passing them to other functions. The idea that we can use tuple types for rest parameters is one place where this is crucial.

例如,以下函数使用元组类型作为剩余参数……

¥For example, the following function that uses a tuple type as a rest parameter…

ts
function foo(...args: [string, number]): void {
// ...
}

……应该与以下函数看起来没有什么不同……

¥…should appear no different from the following function…

ts
function foo(arg0: string, arg1: number): void {
// ...
}

……对于 foo 的任何调用者。

¥…for any caller of foo.

ts
foo("hello", 42);
 
foo("hello", 42, true);
Expected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.
foo("hello");
Expected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.
Try

不过,在一个地方,差异开始变得显而易见:可读性。在第一个例子中,第一个和第二个元素没有参数名称。虽然这些对类型检查没有影响,但元组位置上缺少标签会使它们更难使用。 - 更难传达我们的意图。

¥There is one place where the differences begin to become observable though: readability. In the first example, we have no parameter names for the first and second elements. While these have no impact on type-checking, the lack of labels on tuple positions can make them harder to use - harder to communicate our intent.

这就是 TypeScript 4.0 中元组类型现在可以提供标签的原因。

¥That’s why in TypeScript 4.0, tuples types can now provide labels.

ts
type Range = [start: number, end: number];

为了加深参数列表和元组类型之间的联系,剩余元素和可选元素的语法与参数列表的语法相同。

¥To deepen the connection between parameter lists and tuple types, the syntax for rest elements and optional elements mirrors the syntax for parameter lists.

ts
type Foo = [first: number, second?: string, ...rest: any[]];

使用带标签的元组时有一些规则。其次,标记元组元素时,元组中的所有其他元素也必须标记。

¥There are a few rules when using labeled tuples. For one, when labeling a tuple element, all other elements in the tuple must also be labeled.

ts
type Bar = [first: string, number];
Try

值得注意的是 - 标签不需要我们在解构时为变量指定不同的名称。它们纯粹是为了文档和工具而存在的。

¥It’s worth noting - labels don’t require us to name our variables differently when destructuring. They’re purely there for documentation and tooling.

ts
function foo(x: [first: string, second: number]) {
// ...
 
// note: we didn't need to name these 'first' and 'second'
const [a, b] = x;
a
const a: string
b
const b: number
}
Try

总的来说,带标签的元组在利用元组和参数列表的模式以及以类型安全的方式实现重载时非常方便。事实上,TypeScript 的编辑器支持会尽可能将它们显示为重载。

¥Overall, labeled tuples are handy when taking advantage of patterns around tuples and argument lists, along with implementing overloads in a type-safe way. In fact, TypeScript’s editor support will try to display them as overloads when possible.

Signature help displaying a union of labeled tuples as in a parameter list as two signatures

要了解更多信息,请查看 拉取请求 中的带标签元组元素。

¥To learn more, check out the pull request for labeled tuple elements.

从构造函数推断类属性

¥Class Property Inference from Constructors

当启用 noImplicitAny 时,TypeScript 4.0 现在可以使用控制流分析来确定类中属性的类型。

¥TypeScript 4.0 can now use control flow analysis to determine the types of properties in classes when noImplicitAny is enabled.

ts
class Square {
// Previously both of these were any
area;
(property) Square.area: number
sideLength;
(property) Square.sideLength: number
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}
Try

如果构造函数的路径并非全部都赋值给实例成员,则该属性可能被视为 undefined

¥In cases where not all paths of a constructor assign to an instance member, the property is considered to potentially be undefined.

ts
class Square {
sideLength;
(property) Square.sideLength: number | undefined
 
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
 
get area() {
return this.sideLength ** 2;
Object is possibly 'undefined'.2532Object is possibly 'undefined'.
}
}
Try

如果你有更充分的了解(例如,你有某种 initialize 方法),并且使用的是 strictPropertyInitialization,则仍然需要显式类型注释以及明确的赋值断言 (!)。

¥In cases where you know better (e.g. you have an initialize method of some sort), you’ll still need an explicit type annotation along with a definite assignment assertion (!) if you’re in strictPropertyInitialization.

ts
class Square {
// definite assignment assertion
// v
sideLength!: number;
// type annotation
 
constructor(sideLength: number) {
this.initialize(sideLength);
}
 
initialize(sideLength: number) {
this.sideLength = sideLength;
}
 
get area() {
return this.sideLength ** 2;
}
}
Try

详情请见 查看实现拉取请求

¥For more details, see the implementing pull request.

短路赋值运算符

¥Short-Circuiting Assignment Operators

JavaScript 和许多其他语言都支持一组称为复合赋值运算符的运算符。复合赋值运算符将一个运算符应用于两个参数,然后将结果赋值给左侧。你可能之前见过这些错误:

¥JavaScript, and a lot of other languages, support a set of operators called compound assignment operators. Compound assignment operators apply an operator to two arguments, and then assign the result to the left side. You may have seen these before:

ts
// Addition
// a = a + b
a += b;
// Subtraction
// a = a - b
a -= b;
// Multiplication
// a = a * b
a *= b;
// Division
// a = a / b
a /= b;
// Exponentiation
// a = a ** b
a **= b;
// Left Bit Shift
// a = a << b
a <<= b;

JavaScript 中的许多运算符都有对应的赋值运算符!然而,直到最近,有三个值得注意的例外:逻辑与 (&&)、逻辑或 (||) 和空值合并 (??)。

¥So many operators in JavaScript have a corresponding assignment operator! Up until recently, however, there were three notable exceptions: logical and (&&), logical or (||), and nullish coalescing (??).

这就是为什么 TypeScript 4.0 支持一项新的 ECMAScript 特性,添加了三个新的赋值运算符:&&=||=??=

¥That’s why TypeScript 4.0 supports a new ECMAScript feature to add three new assignment operators: &&=, ||=, and ??=.

这些操作符非常适合替换用户可能编写如下代码的任何示例:

¥These operators are great for substituting any example where a user might write code like the following:

ts
a = a && b;
a = a || b;
a = a ?? b;

或者类似的 if 块,例如:

¥Or a similar if block like

ts
// could be 'a ||= b'
if (!a) {
a = b;
}

我们甚至见过(或者,呃,我们自己编写的)一些模式,仅在需要时才延迟初始化值。

¥There are even some patterns we’ve seen (or, uh, written ourselves) to lazily initialize values, only if they’ll be needed.

ts
let values: string[];
(values ?? (values = [])).push("hello");
// After
(values ??= []).push("hello");

(瞧,我们对我们编写的所有代码并不感到自豪……)

¥(look, we’re not proud of all the code we write…)

在极少数情况下,如果你使用具有副作用的 getter 或 setter,则值得注意的是,这些运算符仅在必要时执行赋值。从这个意义上讲,运算符的右侧不仅是 “short-circuited” - 赋值本身也是如此。

¥On the rare case that you use getters or setters with side-effects, it’s worth noting that these operators only perform assignments if necessary. In that sense, not only is the right side of the operator “short-circuited” - the assignment itself is too.

ts
obj.prop ||= foo();
// roughly equivalent to either of the following
obj.prop || (obj.prop = foo());
if (!obj.prop) {
obj.prop = foo();
}

尝试运行以下示例 可以了解这与始终执行赋值有何不同。

¥Try running the following example to see how that differs from always performing the assignment.

ts
const obj = {
get prop() {
console.log("getter has run");
 
// Replace me!
return Math.random() < 0.5;
},
set prop(_val: boolean) {
console.log("setter has run");
}
};
 
function foo() {
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();
Try

我们要衷心感谢社区成员 Wenlu Wang 的贡献!

¥We’d like to extend a big thanks to community member Wenlu Wang for this contribution!

更多详情,请参阅 查看此处的拉取请求。你也可以 查看 TC39 针对此功能的提案库

¥For more details, you can take a look at the pull request here. You can also check out TC39’s proposal repository for this feature.

catch 子句绑定中的 unknown

¥unknown on catch Clause Bindings

自 TypeScript 诞生以来,catch 子句变量的类型始终为 any。这意味着 TypeScript 允许你对它们执行任何你想做的事情。

¥Since the beginning days of TypeScript, catch clause variables have always been typed as any. This meant that TypeScript allowed you to do anything you wanted with them.

ts
try {
// Do some work
} catch (x) {
// x has type 'any' - have fun!
console.log(x.message);
console.log(x.toUpperCase());
x++;
x.yadda.yadda.yadda();
}
Try

如果我们试图防止错误处理代码中发生更多错误,上面的代码会有一些不良行为!由于这些变量默认的类型为 any,它们缺乏任何类型安全性,否则无效操作可能会导致错误。

¥The above has some undesirable behavior if we’re trying to prevent more errors from happening in our error-handling code! Because these variables have the type any by default, they lack any type-safety which could have errored on invalid operations.

这就是为什么 TypeScript 4.0 现在允许你将 catch 子句变量的类型指定为 unknownunknownany 更安全,因为它提醒我们在操作值之前需要执行某些类型检查。

¥That’s why TypeScript 4.0 now lets you specify the type of catch clause variables as unknown instead. unknown is safer than any because it reminds us that we need to perform some sorts of type-checks before operating on our values.

ts
try {
// ...
} catch (e: unknown) {
// Can't access values on unknowns
console.log(e.toUpperCase());
'e' is of type 'unknown'.18046'e' is of type 'unknown'.
 
if (typeof e === "string") {
// We've narrowed 'e' down to the type 'string'.
console.log(e.toUpperCase());
}
}
Try

虽然 catch 变量的类型默认情况下不会更改,但我们未来可能会考虑添加一个新的 strict 模式标志,以便用户可以选择启用此行为。与此同时,应该可以编写一条 lint 规则,强制 catch 变量具有 : any: unknown 的明确注释。

¥While the types of catch variables won’t change by default, we might consider a new strict mode flag in the future so that users can opt in to this behavior. In the meantime, it should be possible to write a lint rule to force catch variables to have an explicit annotation of either : any or : unknown.

更多详情,请访问 预览此功能的变更

¥For more details you can peek at the changes for this feature.

自定义 JSX 工厂

¥Custom JSX Factories

使用 JSX 时,fragment 是一种 JSX 元素类型,允许我们返回多个子元素。当我们第一次在 TypeScript 中实现片段时,我们对其他库将如何使用它们并没有很好的概念。如今,大多数鼓励使用 JSX 并支持片段的其他库都具有类似的 API 结构。

¥When using JSX, a fragment is a type of JSX element that allows us to return multiple child elements. When we first implemented fragments in TypeScript, we didn’t have a great idea about how other libraries would utilize them. Nowadays most other libraries that encourage using JSX and support fragments have a similar API shape.

在 TypeScript 4.0 中,用户可以通过新的 jsxFragmentFactory 选项自定义片段工厂。

¥In TypeScript 4.0, users can customize the fragment factory through the new jsxFragmentFactory option.

例如,以下 tsconfig.json 文件告诉 TypeScript 以与 React 兼容的方式转换 JSX,但将每个工厂调用切换为 h 而不是 React.createElement,并使用 Fragment 而不是 React.Fragment

¥As an example, the following tsconfig.json file tells TypeScript to transform JSX in a way compatible with React, but switches each factory invocation to h instead of React.createElement, and uses Fragment instead of React.Fragment.

{
"": "esnext",
"": "commonjs",
"": "react",
"": "h",
"": "Fragment"
}
}

如果你需要为每个文件使用不同的 JSX 工厂,你可以利用新的 /** @jsxFrag */ 编译指示注释。例如,以下…

¥In cases where you need to have a different JSX factory on a per-file basis, you can take advantage of the new /** @jsxFrag */ pragma comment. For example, the following…

tsx
// 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 const Header = (
<>
<h1>Welcome</h1>
</>
);
Try

……将转换为以下 JavaScript 输出……

¥…will get transformed to this output JavaScript…

tsx
import React from 'react';
export const Header = (React.createElement(React.Fragment, null,
React.createElement("h1", null, "Welcome")));
 
Try

我们要衷心感谢社区成员 Noj Vek 发送此拉取请求并耐心地与我们的团队合作。

¥We’d like to extend a big thanks to community member Noj Vek for sending this pull request and patiently working with our team on it.

你可以查看 拉取请求 了解更多详情!

¥You can see that the pull request for more details!

build 模式下的速度改进使用 --noEmitOnError

¥Speed Improvements in build mode with --noEmitOnError

以前,在使用 noEmitOnError 标志的情况下,在先前使用 incremental 编译出现错误之后再编译程序会非常慢。这是因为根据 noEmitOnError 标志,上次编译的任何信息都不会缓存在 .tsbuildinfo 文件中。

¥Previously, compiling a program after a previous compile with errors under incremental would be extremely slow when using the noEmitOnError flag. This is because none of the information from the last compilation would be cached in a .tsbuildinfo file based on the noEmitOnError flag.

TypeScript 4.0 对此进行了更改,从而大大提高了这些场景下的速度,并进而改进了 --build 模式场景(这意味着同时使用 incrementalnoEmitOnError)。

¥TypeScript 4.0 changes this which gives a great speed boost in these scenarios, and in turn improves --build mode scenarios (which imply both incremental and noEmitOnError).

详情请见 阅读更多有关拉取请求的信息

¥For details, read up more on the pull request.

带有 --noEmit--incremental

¥--incremental with --noEmit

TypeScript 4.0 允许我们在仍然利用 incremental 编译的同时使用 noEmit 标志。以前不允许这样做,因为 incremental 需要触发 .tsbuildinfo 文件;但是,启用更快增量构建的用例非常重要,值得所有用户启用。

¥TypeScript 4.0 allows us to use the noEmit flag while still leveraging incremental compiles. This was previously not allowed, as incremental needs to emit a .tsbuildinfo files; however, the use-case to enable faster incremental builds is important enough to enable for all users.

更多详情,请参阅 查看实现拉取请求

¥For more details, you can see the implementing pull request.

编辑器改进

¥Editor Improvements

TypeScript 编译器不仅增强了大多数主流编辑器中 TypeScript 本身的编辑体验 - 它还增强了 Visual Studio 系列编辑器等中的 JavaScript 体验。因此,我们的大部分工作都集中在改进编辑器场景上。 - 作为开发者,你大部分时间都花在了这里。

¥The TypeScript compiler doesn’t only power the editing experience for TypeScript itself in most major editors - it also powers the JavaScript experience in the Visual Studio family of editors and more. For that reason, much of our work focuses on improving editor scenarios - the place you spend most of your time as a developer.

在编辑器中使用新的 TypeScript/JavaScript 功能会因编辑器而异,但

¥Using new TypeScript/JavaScript functionality in your editor will differ depending on your editor, but

你可以查看部分 支持 TypeScript 的编辑器列表,以了解更多关于你常用的编辑器是否支持使用新版本的信息。

¥You can check out a partial list of editors that have support for TypeScript to learn more about whether your favorite editor has support to use new versions.

转换为可选链

¥Convert to Optional Chaining

可选链式调用是一项备受喜爱的新特性。这就是 TypeScript 4.0 带来新的重构的原因,它可以将常见模式转换为利用 可选链空值合并

¥Optional chaining is a recent feature that’s received a lot of love. That’s why TypeScript 4.0 brings a new refactoring to convert common patterns to take advantage of optional chaining and nullish coalescing!

Converting a && a.b.c && a.b.c.d.e.f() to a?.b.c?.d.e.f.()

请记住,由于 JavaScript 中真/假判断的微妙性,此重构无法完美捕捉相同的行为,但我们相信它应该能够满足大多数用例的需求,尤其是在 TypeScript 对你的类型有更精确的了解的情况下。

¥Keep in mind that while this refactoring doesn’t perfectly capture the same behavior due to subtleties with truthiness/falsiness in JavaScript, we believe it should capture the intent for most use-cases, especially when TypeScript has more precise knowledge of your types.

详情请见 查看此功能的拉取请求

¥For more details, check out the pull request for this feature.

/** @deprecated */ 支持

¥/** @deprecated */ Support

TypeScript 的编辑支持现在可以识别声明是否已使用 /** @deprecated */ JSDoc 注释标记。该信息显示在完成列表中,并作为编辑可以专门处理的建议诊断。在 VS Code 等编辑器中,弃用的值通常以带删除线的 like this 样式显示。

¥TypeScript’s editing support now recognizes when a declaration has been marked with a /** @deprecated */ JSDoc comment. That information is surfaced in completion lists and as a suggestion diagnostic that editors can handle specially. In an editor like VS Code, deprecated values are typically displayed in a strike-though style like this.

Some examples of deprecated declarations with strikethrough text in the editor

此新功能得益于 Wenlu Wang。有关详细信息,请参阅 拉取请求

¥This new functionality is available thanks to Wenlu Wang. See the pull request for more details.

启动时的部分语义模式

¥Partial Semantic Mode at Startup

我们听到很多用户反映启动时间过长,尤其是在大型项目中。罪魁祸首通常是称为程序构造的过程。这是一个从一组初始根文件开始,解析它们,查找它们的依赖,解析这些依赖,查找这些依赖的依赖,等等的过程。项目越大,获得基本编辑器操作(例如跳转定义或快速信息)所需的等待时间就越长。

¥We’ve heard a lot from users suffering from long startup times, especially on bigger projects. The culprit is usually a process called program construction. This is the process of starting with an initial set of root files, parsing them, finding their dependencies, parsing those dependencies, finding those dependencies’ dependencies, and so on. The bigger your project is, the longer you’ll have to wait before you can get basic editor operations like go-to-definition or quick info.

因此,我们一直在为编辑器开发一种新模式,以便在完整的语言服务体验加载完成之前,提供部分体验。其核心思想是,编辑器可以运行一个轻量级的部分服务器,该服务器仅查看编辑器当前打开的文件。

¥That’s why we’ve been working on a new mode for editors to provide a partial experience until the full language service experience has loaded up. The core idea is that editors can run a lightweight partial server that only looks at the current files that the editor has open.

很难确切地说你将看到哪些改进,但据传闻,TypeScript 过去通常需要 20 秒到 1 分钟的时间才能在 Visual Studio Code 代码库上完全响应。相比之下,我们新的部分语义模式似乎将延迟缩短至几秒钟。例如,在下面的视频中,你可以看到两个并排显示的编辑器,左侧运行 TypeScript 3.9,右侧运行 TypeScript 4.0。

¥It’s hard to say precisely what sorts of improvements you’ll see, but anecdotally, it used to take anywhere between 20 seconds to a minute before TypeScript would become fully responsive on the Visual Studio Code codebase. In contrast, our new partial semantic mode seems to bring that delay down to just a few seconds. As an example, in the following video, you can see two side-by-side editors with TypeScript 3.9 running on the left and TypeScript 4.0 running on the right.

在特别大的代码库上重新启动两个编辑器时,使用 TypeScript 3.9 的编辑器根本无法提供补全或快速信息。另一方面,尽管在后台加载了整个项目,但使用 TypeScript 4.0 的编辑器可以立即为我们正在编辑的当前文件提供丰富的体验。

¥When restarting both editors on a particularly large codebase, the one with TypeScript 3.9 can’t provide completions or quick info at all. On the other hand, the editor with TypeScript 4.0 can immediately give us a rich experience in the current file we’re editing, despite loading the full project in the background.

目前唯一支持此模式的编辑器是 Visual Studio Code,它将在 Visual Studio Code 内部人员 中提供一些用户体验改进。我们意识到,这种体验在用户体验和功能方面可能仍有改进空间,因此我们已将 改进列表 纳入考量。我们期待你提供更多反馈,了解你认为可能有用的内容。

¥Currently the only editor that supports this mode is Visual Studio Code which has some UX improvements coming up in Visual Studio Code Insiders. We recognize that this experience may still have room for polish in UX and functionality, and we have a list of improvements in mind. We’re looking for more feedback on what you think might be useful.

更多信息,你可以将 查看原始提案实现拉取请求后续元问题 一起使用。

¥For more information, you can see the original proposal, the implementing pull request, along with the follow-up meta issue.

更智能的自动导入

¥Smarter Auto-Imports

自动导入是一项非常棒的功能,它使编码变得非常简单;但是,每次自动导入似乎不起作用时,都会给用户带来很大的困扰。我们从用户那里听到的一个具体问题是,自动导入对用 TypeScript 编写的依赖不起作用。 - 也就是说,除非他们在项目中的其他地方至少编写了一个显式导入语句。

¥Auto-import is a fantastic feature that makes coding a lot easier; however, every time auto-import doesn’t seem to work, it can throw users off a lot. One specific issue that we heard from users was that auto-imports didn’t work on dependencies that were written in TypeScript - that is, until they wrote at least one explicit import somewhere else in their project.

为什么自动导入对 @types 包有效,而对自带类型的包无效?事实证明,自动导入仅适用于项目已包含的包。由于 TypeScript 有一些特殊的默认设置,会自动将 node_modules/@types 中的包添加到你的项目中,因此这些包会被自动导入。另一方面,其他软件包被排除在外,因为遍历所有 node_modules 软件包的开销可能非常大。

¥Why would auto-imports work for @types packages, but not for packages that ship their own types? It turns out that auto-imports only work on packages your project already includes. Because TypeScript has some quirky defaults that automatically add packages in node_modules/@types to your project, those packages would be auto-imported. On the other hand, other packages were excluded because crawling through all your node_modules packages can be really expensive.

所有这些,当你尝试自动导入刚安装但尚未使用的东西时,都会导致相当糟糕的入门体验。

¥All of this leads to a pretty lousy getting started experience for when you’re trying to auto-import something that you’ve just installed but haven’t used yet.

TypeScript 4.0 现在在编辑器场景中做了一些额外的工作,以包含你在 package.jsondependencies(和 peerDependencies)字段中列出的包。这些包中的信息仅用于改进自动导入,不会改变类型检查等其他任何功能。这使得我们可以为所有具有类型的依赖提供自动导入,而无需承担完整 node_modules 搜索的成本。

¥TypeScript 4.0 now does a little extra work in editor scenarios to include the packages you’ve listed in your package.json’s dependencies (and peerDependencies) fields. The information from these packages is only used to improve auto-imports, and doesn’t change anything else like type-checking. This allows us to provide auto-imports for all of your dependencies that have types, without incurring the cost of a complete node_modules search.

在极少数情况下,当你的 package.json 列出了十个以上尚未导入的类型化依赖时,此功能会自动禁用,以防止项目加载缓慢。要强制该功能生效或完全禁用它,你应该能够配置你的编辑器。对于 Visual Studio Code,这是 “包含包 JSON 自动导入”(或 typescript.preferences.includePackageJsonAutoImports)设置。

¥In the rare cases when your package.json lists more than ten typed dependencies that haven’t been imported yet, this feature automatically disables itself to prevent slow project loading. To force the feature to work, or to disable it entirely, you should be able to configure your editor. For Visual Studio Code, this is the “Include Package JSON Auto Imports” (or typescript.preferences.includePackageJsonAutoImports) setting.

Configuring 'include package JSON auto imports'

更多详情,请参阅 提案问题实现拉取请求

¥For more details, you can see the proposal issue along with the implementing pull request.

我们的新网站!

¥Our New Website!

TypeScript 网站 最近已彻底重写并正式发布!

¥The TypeScript website has recently been rewritten from the ground up and rolled out!

A screenshot of the new TypeScript website

我们已经写了一些关于我们新网站的内容,你可以在那里了解更多信息;但值得一提的是,我们仍然希望听到你的想法!如果你有任何问题、意见或建议,你可以使用 将它们提交到网站的问题跟踪器上

¥We already wrote a bit about our new site, so you can read up more there; but it’s worth mentioning that we’re still looking to hear what you think! If you have questions, comments, or suggestions, you can file them over on the website’s issue tracker.

打破变更

¥Breaking Changes

lib.d.ts 变更

¥lib.d.ts Changes

我们的 lib.d.ts 声明已更改 - 更具体地说,DOM 的类型已经发生了变化。最显著的变化可能是删除了 document.origin,它只在旧版本的 IE 和 Safari 中有效,MDN 建议迁移到 self.origin

¥Our lib.d.ts declarations have changed - most specifically, types for the DOM have changed. The most notable change may be the removal of document.origin which only worked in old versions of IE and Safari MDN recommends moving to self.origin.

属性覆盖访问器(反之亦然)是错误的

¥Properties Overriding Accessors (and vice versa) is an Error

以前,使用 useDefineForClassFields 时,只有属性覆盖访问器,或访问器覆盖属性时才会报错;然而,TypeScript 现在在派生类中声明属性时,如果该属性会覆盖基类中的 getter 或 setter,则总是会报错。

¥Previously, it was only an error for properties to override accessors, or accessors to override properties, when using useDefineForClassFields; however, TypeScript now always issues an error when declaring a property in a derived class that would override a getter or setter in the base class.

ts
class Base {
get foo() {
return 100;
}
set foo(value) {
// ...
}
}
 
class Derived extends Base {
foo = 10;
'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.
}
Try
ts
class Base {
prop = 10;
}
 
class Derived extends Base {
get prop() {
'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.
return 100;
}
}
Try

查看有关 实现拉取请求 的更多详细信息。

¥See more details on the implementing pull request.

delete 的操作数必须是可选的。

¥Operands for delete must be optional.

strictNullChecks 中使用 delete 运算符时,操作数现在必须是 anyunknownnever 或可选(即类型中包含 undefined)。否则,使用 delete 运算符将导致错误。

¥When using the delete operator in strictNullChecks, the operand must now be any, unknown, never, or be optional (in that it contains undefined in the type). Otherwise, use of the delete operator is an error.

ts
interface Thing {
prop: string;
}
 
function f(x: Thing) {
delete x.prop;
The operand of a 'delete' operator must be optional.2790The operand of a 'delete' operator must be optional.
}
Try

查看有关 实现拉取请求 的更多详细信息。

¥See more details on the implementing pull request.

TypeScript 的 Node Factory 已弃用

¥Usage of TypeScript’s Node Factory is Deprecated

目前,TypeScript 提供了一组用于生成 AST 节点的 “factory” 函数;但是,TypeScript 4.0 提供了一个新的节点工厂 API。因此,在 TypeScript 4.0 中,我们决定弃用这些旧函数,转而使用新函数。

¥Today TypeScript provides a set of “factory” functions for producing AST Nodes; however, TypeScript 4.0 provides a new node factory API. As a result, for TypeScript 4.0 we’ve made the decision to deprecate these older functions in favor of the new ones.

详情请见 阅读与此变更相关的拉取请求

¥For more details, read up on the relevant pull request for this change.