TypeScript 4.3

属性的单独写入类型

🌐 Separate Write Types on Properties

在 JavaScript 中,API 在存储之前转换传入的值是相当常见的。这种情况在 getter 和 setter 中也经常发生。例如,假设我们有一个类,其 setter 总是将一个值转换为 number 后再保存到私有字段中。

🌐 In JavaScript, it’s pretty common for APIs to convert values that are passed in before storing them. This often happens with getters and setters too. For example, let’s imagine we’ve got a class with a setter that always converts a value into a number before saving it in a private field.

js
class Thing {
#size = 0;
 
get size() {
return this.#size;
}
set size(value) {
let num = Number(value);
 
// Don't allow NaN and stuff.
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}
 
this.#size = num;
}
}
Try

我们如何在 TypeScript 中编写这段 JavaScript 代码? 事实上,从技术上讲,我们不需要做任何特别的处理——TypeScript 可以在没有显式类型的情况下查看这段代码,并能够推断出 size 是一个数字。

🌐 How would we type this JavaScript code in TypeScript? Well, technically we don’t have to do anything special here - TypeScript can look at this with no explicit types and can figure out that size is a number.

问题在于 size 允许你给它分配的不仅仅是 number。 我们可以通过说 size 的类型是 unknownany 来解决这个问题,就像下面的代码片段一样:

🌐 The problem is that size allows you to assign more than just numbers to it. We could get around this by saying that size has the type unknown or any like in this snippet:

ts
class Thing {
// ...
get size(): unknown {
return this.#size;
}
}

但那不行——unknown 会迫使阅读 size 的人进行类型断言,而 any 无法捕捉任何错误。 如果我们真的想要建模转换值的 API,TypeScript 之前的版本迫使我们在精确(这让读取值更容易,但写入更难)和宽容(这让写入值更容易,但读取更难)之间做出选择。

🌐 But that’s no good - unknown forces people reading size to do a type assertion, and any won’t catch any mistakes. If we really want to model APIs that convert values, previous versions of TypeScript forced us to pick between being precise (which makes reading values easier, and writing harder) and being permissive (which makes writing values easier, and reading harder).

这就是为什么 TypeScript 4.3 允许你指定属性的读写类型。

🌐 That’s why TypeScript 4.3 allows you to specify types for reading and writing to properties.

ts
class Thing {
#size = 0;
 
get size(): number {
return this.#size;
}
 
set size(value: string | number | boolean) {
let num = Number(value);
 
// Don't allow NaN and stuff.
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}
 
this.#size = num;
}
}
Try

在上面的例子中,我们的 set 访问器可以接受更广泛的类型(stringbooleannumber),但是我们的 get 访问器始终保证它将是一个 number。 现在我们终于可以将其他类型分配给这些属性而不会出现错误了!

🌐 In the above example, our set accessor takes a broader set of types (strings, booleans, and numbers), but our get accessor always guarantees it will be a number. Now we can finally assign other types to these properties with no errors!

ts
let thing = new Thing();
 
// Assigning other types to `thing.size` works!
thing.size = "hello";
thing.size = true;
thing.size = 42;
 
// Reading `thing.size` always produces a number!
let mySize: number = thing.size;
Try

在考虑两个具有相同名称的属性如何相互关联时,TypeScript 只会使用“读取”类型(例如上面 get 访问器的类型)。 “写入”类型仅在直接向属性写入时才会被考虑。

🌐 When considering how two properties with the same name relate to each other, TypeScript will only use the “reading” type (e.g. the type on the get accessor above). “Writing” types are only considered when directly writing to a property.

请记住,这并不是仅限于类的模式。你可以在对象字面量中使用不同类型编写 getter 和 setter。

🌐 Keep in mind, this isn’t a pattern that’s limited to classes. You can write getters and setters with different types in object literals.

ts
function makeThing(): Thing {
let size = 0;
return {
get size(): number {
return size;
},
set size(value: string | number | boolean) {
let num = Number(value);
// Don't allow NaN and stuff.
if (!Number.isFinite(num)) {
size = 0;
return;
}
size = num;
},
};
}

事实上,我们为接口/对象类型添加了语法,以支持对属性进行不同的读/写类型。

🌐 In fact, we’ve added syntax to interfaces/object types to support different reading/writing types on properties.

ts
// Now valid!
interface Thing {
get size(): number
set size(value: number | string | boolean);
}

使用不同类型进行属性读写的一个限制是,用于读取属性的类型必须可以赋值给用于写入的类型。换句话说,getter 的类型必须可以赋值给 setter。这确保了一定程度的一致性,使得一个属性总是可以赋值给自身。

🌐 One limitation of using different types for reading and writing properties is that the type for reading a property has to be assignable to the type that you’re writing. In other words, the getter type has to be assignable to the setter. This ensures some level of consistency, so that a property is always assignable to itself.

有关此功能的更多信息,请查看实现的拉取请求

🌐 For more information on this feature, take a look at the implementing pull request.

override--noImplicitOverride 标志

🌐 override and the --noImplicitOverride Flag

在 JavaScript 中扩展类时,语言使得重写方法变得非常容易(双关语:super),但不幸的是,你可能会遇到一些错误。

🌐 When extending classes in JavaScript, the language makes it super easy (pun intended) to override methods - but unfortunately, there are some mistakes that you can run into.

一个大问题是缺少重命名功能。例如,看看以下类:

🌐 One big one is missing renames. For example, take the following classes:

ts
class SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

SpecializedComponentSomeComponent 的子类,并重写了 showhide 方法。如果有人决定删除 showhide 并用一个方法替换,会发生什么情况?

diff
class SomeComponent {
- show() {
- // ...
- }
- hide() {
- // ...
- }
+ setVisible(value: boolean) {
+ // ...
+ }
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

哦不! 我们的 SpecializedComponent 没有更新。 现在它只是添加了这两个可能不会被调用的无用方法 showhide

🌐 Oh no! Our SpecializedComponent didn’t get updated. Now it’s just adding these two useless show and hide methods that probably won’t get called.

这里问题的一部分在于,用户无法明确表示他们是想添加一个新方法,还是想重写一个现有方法。这就是 TypeScript 4.3 引入 override 关键字的原因。

🌐 Part of the issue here is that a user can’t make it clear whether they meant to add a new method, or to override an existing one. That’s why TypeScript 4.3 adds the override keyword.

ts
class SpecializedComponent extends SomeComponent {
override show() {
// ...
}
override hide() {
// ...
}
}

当一个方法被标记为 override 时,TypeScript 会始终确保基类中存在一个同名方法。

🌐 When a method is marked with override, TypeScript will always make sure that a method with the same name exists in a the base class.

ts
class SomeComponent {
setVisible(value: boolean) {
// ...
}
}
class SpecializedComponent extends SomeComponent {
override show() {
This member cannot have an 'override' modifier because it is not declared in the base class 'SomeComponent'.4113This member cannot have an 'override' modifier because it is not declared in the base class 'SomeComponent'.
 
}
}
Try

这是一个很大的改进,但如果你忘记在方法上写 override,这也无济于事——而且这是用户也可能遇到的一个重大错误。

🌐 This is a big improvement, but it doesn’t help if you forget to write override on a method - and that’s a big mistake users can run into also.

例如,你可能会在不知情的情况下意外“覆盖”基类中存在的方法。

🌐 For example, you might accidentally “trample over” a method that exists in a base class without realizing it.

ts
class Base {
someHelperMethod() {
// ...
}
}
class Derived extends Base {
// Oops! We weren't trying to override here,
// we just needed to write a local helper method.
someHelperMethod() {
// ...
}
}

这就是为什么 TypeScript 4.3 提供了一个新的 noImplicitOverride 标志。当启用此选项时,除非明确使用 override 关键字,否则覆盖任何超类的方法都会导致错误。在最后一个示例中,根据 noImplicitOverride,TypeScript 会报错,并提示我们可能需要在 Derived 内重命名我们的方法。

🌐 That’s why TypeScript 4.3 also provides a new noImplicitOverride flag. When this option is turned on, it becomes an error to override any method from a superclass unless you explicitly use an override keyword. In that last example, TypeScript would error under noImplicitOverride, and give us a clue that we probably need to rename our method inside of Derived.

我们想向社区对本次实现表示感谢。这些功能的工作是由 Wenlu Wang一个 pull request 中实现的,尽管之前由 Paul Cody Johnston 提交的仅实现 override 关键字的 pull request 为方向和讨论提供了基础。我们感谢大家为这些功能投入的时间。

🌐 We’d like to extend our thanks to our community for the implementation here. The work for these items was implemented in a pull request by Wenlu Wang, though an earlier pull request implementing only the override keyword by Paul Cody Johnston served as a basis for direction and discussion. We extend our gratitude for putting in the time for these features.

模板字符串类型改进

🌐 Template String Type Improvements

在最近的版本中,TypeScript 引入了一种新的类型构造:模板字符串类型。这些类型通过连接…来构造新的类似字符串的类型。

🌐 In recent versions, TypeScript introduced a new type construct: template string types. These are types that either construct new string-like types by concatenating…

ts
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";

……或匹配其他类似字符串类型的模式。

ts
declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
// Works!
s1 = s2;

我们做的第一个改变只是 TypeScript 推断模板字符串类型的时间。当一个模板字符串被字符串字面量类型上下文类型化时(即当 TypeScript 看到我们将模板字符串传递给需要字面量类型的东西时),它会尝试给该表达式一个模板类型。

🌐 The first change we made is just in when TypeScript will infer a template string type. When a template string is contextually typed by a string-literal-like type (i.e. when TypeScript sees we’re passing a template string to something that takes a literal type) it will try to give that expression a template type.

ts
function bar(s: string): `hello ${string}` {
// Previously an error, now works!
return `hello ${s}`;
}

这在推断类型时也会起作用,类型参数 extends string

🌐 This also kicks in when inferring types, and the type parameter extends string

ts
declare let s: string;
declare function f<T extends string>(x: T): T;
// Previously: string
// Now : `hello ${string}`
let x2 = f(`hello ${s}`);

这里的第二个重大变化是 TypeScript 现在可以更好地关联不同的模板字符串类型,并在它们之间进行推断。

🌐 The second major change here is that TypeScript can now better-relate, and infer between, different template string types.

要查看此示例代码,请参阅以下示例代码:

🌐 To see this, take the following example code:

ts
declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
s1 = s2;
s1 = s3;

在检查像 s2 这样的字符串字面量类型时,TypeScript 可以匹配字符串内容,并判断 s2 在第一次赋值时与 s1 兼容;然而,一旦它遇到另一个模板字符串,它就会放弃。因此,像将 s3 赋值给 s1 这样的操作根本无法工作。

🌐 When checking against a string literal type like on s2, TypeScript could match against the string contents and figure out that s2 was compatible with s1 in the first assignment; however, as soon as it saw another template string, it just gave up. As a result, assignments like s3 to s1 just didn’t work.

TypeScript 现在实际上会去验证模板字符串的每一部分是否都能成功匹配。你现在可以混合使用不同替换的模板字符串,TypeScript 会很好地判断它们是否真的兼容。

🌐 TypeScript now actually does the work to prove whether or not each part of a template string can successfully match. You can now mix and match template strings with different substitutions and TypeScript will do a good job to figure out whether they’re really compatible.

ts
declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
declare let s4: `1-${number}-3`;
declare let s5: `1-2-${number}`;
declare let s6: `${number}-2-${number}`;
// Now *all of these* work!
s1 = s2;
s1 = s3;
s1 = s4;
s1 = s5;
s1 = s6;

在进行这项工作时,我们也确保增强了推断能力。你可以看到这些功能的实际示例:

🌐 In doing this work, we were also sure to add better inference capabilities. You can see an example of these in action:

ts
declare function foo<V extends string>(arg: `*${V}*`): V;
function test<T extends string>(s: string, n: number, b: boolean, t: T) {
let x1 = foo("*hello*"); // "hello"
let x2 = foo("**hello**"); // "*hello*"
let x3 = foo(`*${s}*` as const); // string
let x4 = foo(`*${n}*` as const); // `${number}`
let x5 = foo(`*${b}*` as const); // "true" | "false"
let x6 = foo(`*${t}*` as const); // `${T}`
let x7 = foo(`**${s}**` as const); // `*${string}*`
}

欲了解更多信息,请参阅关于利用上下文类型的原始拉取请求,以及改进模板类型之间推断和检查的拉取请求

🌐 For more information, see the original pull request on leveraging contextual types, along with the pull request that improved inference and checking between template types.

ECMAScript #private 类元素

🌐 ECMAScript #private Class Elements

TypeScript 4.3 扩展了类中哪些元素可以被赋予 #private #names,以使它们在运行时真正私有。除了属性之外,方法和访问器也可以被赋予私有名称。

🌐 TypeScript 4.3 expands which elements in a class can be given #private #names to make them truly private at run-time. In addition to properties, methods and accessors can also be given private names.

ts
class Foo {
#someMethod() {
//...
}
get #someValue() {
return 100;
}
publicMethod() {
// These work.
// We can access private-named members inside this class.
this.#someMethod();
return this.#someValue;
}
}
new Foo().#someMethod();
// ~~~~~~~~~~~
// error!
// Property '#someMethod' is not accessible
// outside class 'Foo' because it has a private identifier.
new Foo().#someValue;
// ~~~~~~~~~~
// error!
// Property '#someValue' is not accessible
// outside class 'Foo' because it has a private identifier.

更广泛地说,静态成员现在也可以拥有私有名称了。

🌐 Even more broadly, static members can now also have private names.

ts
class Foo {
static #someMethod() {
// ...
}
}
Foo.#someMethod();
// ~~~~~~~~~~~
// error!
// Property '#someMethod' is not accessible
// outside class 'Foo' because it has a private identifier.

此功能由我们在彭博社的朋友们通过 拉取请求 编写——由 Titian Cernicova-DragomirKubilay Kahveci 撰写,并得到 Joey WattsRob PalmerTim McClure 的支持与专业指导。 我们想向他们所有人表示感谢!

🌐 This feature was authored in a pull request from our friends at Bloomberg - written by Titian Cernicova-Dragomirand Kubilay Kahveci, with support and expertise from Joey Watts, Rob Palmer, and Tim McClure. We’d like to extend our thanks to all of them!

ConstructorParameters 适用于抽象类

🌐 ConstructorParameters Works on Abstract Classes

在 TypeScript 4.3 中,ConstructorParameters 类型助手现在可以用于 abstract 类。

🌐 In TypeScript 4.3, the ConstructorParameters type helper now works on abstract classes.

ts
abstract class C {
constructor(a: string, b: number) {
// ...
}
}
// Has the type '[a: string, b: number]'.
type CParams = ConstructorParameters<typeof C>;

这要归功于 TypeScript 4.2 中的改进,其中构造签名可以标记为抽象:

🌐 This is thanks to work done in TypeScript 4.2, where construct signatures can be marked as abstract:

ts
type MyConstructorOf<T> = {
abstract new(...args: any[]): T;
}
// or using the shorthand syntax:
type MyConstructorOf<T> = abstract new (...args: any[]) => T;

你可以在 GitHub 上查看更多详细的更改

🌐 You can see the change in more detail on GitHub.

泛型的上下文收缩

🌐 Contextual Narrowing for Generics

TypeScript 4.3 现在在泛型值上包含了一些稍微智能的类型缩小逻辑。这使得 TypeScript 能够接受更多的模式,有时甚至可以发现错误。

🌐 TypeScript 4.3 now includes some slightly smarter type-narrowing logic on generic values. This allows TypeScript to accept more patterns, and sometimes even catch mistakes.

为了激励一下,我们假设我们正在尝试编写一个名为 makeUnique 的函数。它会接受一个元素的 SetArray,如果它接收到一个 Array,它将对该 Array 进行排序,并根据某个比较函数去除重复项。在完成所有这些操作后,它将返回原始集合。

🌐 For some motivation, let’s say we’re trying to write a function called makeUnique. It’ll take a Set or an Array of elements, and if it’s given an Array, it’ll sort that Array remove duplicates according to some comparison function. After all that, it will return the original collection.

ts
function makeUnique<T>(
collection: Set<T> | T[],
comparer: (x: T, y: T) => number
): Set<T> | T[] {
// Early bail-out if we have a Set.
// We assume the elements are already unique.
if (collection instanceof Set) {
return collection;
}
// Sort the array, then remove consecutive duplicates.
collection.sort(comparer);
for (let i = 0; i < collection.length; i++) {
let j = i;
while (
j < collection.length &&
comparer(collection[i], collection[j + 1]) === 0
) {
j++;
}
collection.splice(i + 1, j - i);
}
return collection;
}

让我们先把关于这个函数实现的问题放一边,假设它是由更广泛应用的需求产生的。你可能注意到的一点是,函数签名没有捕捉到 collection 的原始类型。我们可以通过添加一个类型参数 C 来实现这一点,替代我们之前写的 Set<T> | T[]

diff
- function makeUnique<T>(collection: Set<T> | T[], comparer: (x: T, y: T) => number): Set<T> | T[]
+ function makeUnique<T, C extends Set<T> | T[]>(collection: C, comparer: (x: T, y: T) => number): C

在 TypeScript 4.2 及更早版本中,一旦尝试这样做,就会出现一堆错误。

🌐 In TypeScript 4.2 and earlier, you’d end up with a bunch of errors as soon as you tried this.

ts
function makeUnique<T, C extends Set<T> | T[]>(
collection: C,
comparer: (x: T, y: T) => number
): C {
// Early bail-out if we have a Set.
// We assume the elements are already unique.
if (collection instanceof Set) {
return collection;
}
// Sort the array, then remove consecutive duplicates.
collection.sort(comparer);
// ~~~~
// error: Property 'sort' does not exist on type 'C'.
for (let i = 0; i < collection.length; i++) {
// ~~~~~~
// error: Property 'length' does not exist on type 'C'.
let j = i;
while (
j < collection.length &&
comparer(collection[i], collection[j + 1]) === 0
) {
// ~~~~~~
// error: Property 'length' does not exist on type 'C'.
// ~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
// error: Element implicitly has an 'any' type because expression of type 'number'
// can't be used to index type 'Set<T> | T[]'.
j++;
}
collection.splice(i + 1, j - i);
// ~~~~~~
// error: Property 'splice' does not exist on type 'C'.
}
return collection;
}

呃,出错了!TypeScript 为什么对我们这么苛刻?

🌐 Ew, errors! Why is TypeScript being so mean to us?

问题在于,当我们执行 collection instanceof Set 检查时,我们期望它作为类型保护,将类型从 Set<T> | T[] 根据所处的分支缩小到 Set<T>T[];然而,我们处理的不是 Set<T> | T[],而是试图缩小通用值 collection 的类型,而它的类型是 C

这是一个非常微妙的区别,但它确实会产生影响。TypeScript 不能仅仅抓取 C 的约束(即 Set<T> | T[])并进行缩小。如果 TypeScript 真的 尝试从 Set<T> | T[] 缩小,它会忘记 collection 在每个分支中也是 C,因为没有简单的方法来保留该信息。如果假设性地 TypeScript 尝试那种方法,它会以不同的方式破坏上述示例。在返回位置,当函数期望的值类型是 C 时,我们在每个分支中实际上会得到 Set<T>T[],TypeScript 会拒绝这样做。

ts
function makeUnique<T>(
collection: Set<T> | T[],
comparer: (x: T, y: T) => number
): Set<T> | T[] {
// Early bail-out if we have a Set.
// We assume the elements are already unique.
if (collection instanceof Set) {
return collection;
// ~~~~~~~~~~
// error: Type 'Set<T>' is not assignable to type 'C'.
// 'Set<T>' is assignable to the constraint of type 'C', but
// 'C' could be instantiated with a different subtype of constraint 'Set<T> | T[]'.
}
// ...
return collection;
// ~~~~~~~~~~
// error: Type 'T[]' is not assignable to type 'C'.
// 'T[]' is assignable to the constraint of type 'C', but
// 'C' could be instantiated with a different subtype of constraint 'Set<T> | T[]'.
}

那么 TypeScript 4.3 会带来哪些改变呢? 基本上,在编写代码的几个关键地方,类型系统真正关心的只是类型的约束。 例如,当我们写 collection.length 时,TypeScript 并不关注 collection 的类型是 C,它只关心可用的属性,而这些属性是由约束 T[] | Set<T> 决定的。

🌐 So how does TypeScript 4.3 change things? Well, basically in a few key places when writing code, all the type system really cares about is the constraint of a type. For example, when we write collection.length, TypeScript doesn’t care about the fact that collection has the type C, it only cares about the properties available, which are determined by the constraint T[] | Set<T>.

在这种情况下,TypeScript 会采用约束的缩小类型,因为那能提供你关心的数据;然而,在其他任何情况下,我们都会尝试缩小原始的泛型类型(并且通常最终得到的还是原始的泛型类型)。

🌐 In cases like this, TypeScript will grab the narrowed type of the constraint because that will give you the data you care about; however, in any other case, we’ll just try to narrow the original generic type (and often end up with the original generic type).

换句话说,TypeScript 会根据你如何使用泛型值,对其进行略有不同的类型缩小。最终结果是,上述整个示例可以编译,通过类型检查时不会产生任何错误。

🌐 In other words, based on how you use a generic value, TypeScript will narrow it a little differently. The end result is that the entire above example compiles with no type-checking errors.

欲了解更多详情,你可以查看 GitHub 上的原始拉取请求

🌐 For more details, you can look at the original pull request on GitHub.

始终为真的 Promise 检查

🌐 Always-Truthy Promise Checks

根据 strictNullChecks,在条件语句中检查 Promise 是否为“真值”将会触发错误。

🌐 Under strictNullChecks, checking whether a Promise is “truthy” in a conditional will trigger an error.

ts
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) {
// ~~~~~
// Error!
// This condition will always return true since
// this 'Promise<boolean>' appears to always be defined.
// Did you forget to use 'await'?
return "true";
}
return "false";
}

此更改 是由 Jack Works 提供的,我们向他们表示感谢!

static 索引签名

🌐 static Index Signatures

索引签名允许我们在值上设置比类型明确声明的更多的属性。

🌐 Index signatures allow us to set more properties on a value than a type explicitly declares.

ts
class Foo {
hello = "hello";
world = 1234;
// This is an index signature:
[propName: string]: string | number | undefined;
}
let instance = new Foo();
// Valid assignment
instance["whatever"] = 42;
// Has type 'string | number | undefined'.
let x = instance["something"];

直到现在,索引签名只能在类的实例端声明。得益于Wenlu Wang拉取请求,现在索引签名也可以声明为 static

🌐 Up until now, an index signature could only be declared on the instance side of a class. Thanks to a pull request from Wenlu Wang, index signatures can now be declared as static.

ts
class Foo {
static hello = "hello";
static world = 1234;
static [propName: string]: string | number | undefined;
}
// Valid.
Foo["whatever"] = 42;
// Has type 'string | number | undefined'
let x = Foo["something"];

类的静态侧的索引签名适用与实例侧相同的规则——也就是说,每个其他静态属性都必须与索引签名兼容。

🌐 The same sorts of rules apply for index signatures on the static side of a class as they do for the instance side - namely, that every other static property has to be compatible with the index signature.

ts
class Foo {
static prop = true;
// ~~~~
// Error! Property 'prop' of type 'boolean'
// is not assignable to string index type
// 'string | number | undefined'.
static [propName: string]: string | number | undefined;
}

.tsbuildinfo 尺寸改进

🌐 .tsbuildinfo Size Improvements

在 TypeScript 4.3 中,作为 incremental 构建一部分生成的 .tsbuildinfo 文件应该会显著更小。这得益于内部格式的多项优化,创建了使用数字标识符的表格,以在整个文件中使用,而不是重复完整路径和类似信息。这项工作由 Tobias Koppers他们的拉取请求 中牵头,成为 随后的拉取请求进一步优化 的灵感来源。

🌐 In TypeScript 4.3, .tsbuildinfo files that are generated as part of incremental builds should be significantly smaller. This is thanks to several optimizations in the internal format, creating tables with numeric identifiers to be used throughout the file instead of repeating full paths and similar information. This work was spear-headed by Tobias Koppers in their pull request, serving as inspiration for the ensuing pull request and further optimizations.

我们已经看到 .tsbuildinfo 文件大小显著减少,包括

🌐 We have seen significant reductions of .tsbuildinfo file sizes including

  • 1MB 到 411 KB
  • 14.9MB 到 1MB
  • 1345MB 到 467MB

毋庸置疑,这些大小上的节省也意味着构建时间会略微加快。

🌐 Needless to say, these sorts of savings in size translate to slightly faster build times as well.

--incremental--watch 编译中的延迟计算

🌐 Lazier Calculations in --incremental and --watch Compilations

关于incremental--watch模式的一个问题是,虽然它们可以加快后续的编译速度,但初次编译可能会稍慢——在某些情况下甚至明显更慢。 这是因为这些模式需要执行大量的记录工作,计算当前项目的信息,有时还需要将这些数据保存在.tsbuildinfo文件中以供后续构建使用。

🌐 One of the issues with incremental and --watch modes are that while they make later compilations go faster, the initial compilation can be a bit slower - in some cases, significantly slower. This is because these modes have to perform a bunch of book-keeping, computing information about the current project, and sometimes saving that data in a .tsbuildinfo file for later builds.

这就是为什么除了 .tsbuildinfo 大小改进之外,TypeScript 4.3 还引入了一些对 incremental--watch 模式的更改,使得使用这些标志进行项目的首次构建与普通构建一样快! 为此,通常会预先计算的大部分信息现在改为在后续构建时按需计算。 虽然这可能会给后续构建增加一些开销,但 TypeScript 的 incremental--watch 功能通常仍然只处理更小的一部分文件,并且所需的信息会在之后保存。 从某种意义上说,incremental--watch 构建在你多次更新文件后,会“预热”并使文件编译速度加快。

🌐 That’s why on top of .tsbuildinfo size improvements, TypeScript 4.3 also ships some changes to incremental and --watch modes that make the first build of a project with these flags just as fast as an ordinary build! To do this, much of the information that would ordinarily be computed up-front is instead done on an on-demand basis for later builds. While this can add some overhead to a subsequent build, TypeScript’s incremental and --watch functionality will still typically operate on a much smaller set of files, and any needed information will be saved afterwards. In a sense, incremental and --watch builds will “warm up” and get faster at compiling files once you’ve updated them a few times.

在一个有3000个文件的仓库中,这将初始构建时间几乎减少到三分之一

🌐 In a repository with 3000 files, this reduced initial build times to almost a third!

[这项工作由 Tobias Koppers 开始](https://github.com/microsoft/TypeScript/pull/42960),他的工作最终促成了 该功能的最终变更。我们想向 Tobias 表示衷心的感谢,感谢他帮助我们发现这些改进的机会!

导入语句补全

🌐 Import Statement Completions

用户在 JavaScript 中使用导入和导出语句时遇到的最大痛点之一是顺序——特别是导入语句的书写顺序

🌐 One of the biggest pain-points users run into with import and export statements in JavaScript is the order - specifically that imports are written as

ts
import { func } from "./module.js";

而不是

🌐 instead of

ts
from "./module.js" import { func };

这会在从零开始写完整的导入语句时造成一些困扰,因为自动补全无法正确工作。例如,如果你开始输入类似 import { 的内容,TypeScript 根本不知道你打算从哪个模块导入,因此无法提供任何范围缩小的补全建议。

🌐 This causes some pain when writing out a full import statement from scratch because auto-complete wasn’t able to work correctly. For example, if you start writing something like import {, TypeScript has no idea what module you’re planning on importing from, so it couldn’t provide any scoped-down completions.

为了解决这个问题,我们利用了自动导入的强大功能! 自动导入已经可以处理无法从特定模块缩小补全范围的问题——它们的整个目的就是提供所有可能的导出,并在文件顶部自动插入导入语句。

🌐 To alleviate this, we’ve leveraged the power of auto-imports! Auto-imports already deal with the issue of not being able to narrow down completions from a specific module - their whole point is to provide every possible export and automatically insert an import statement at the top of your file.

所以现在当你开始编写一个没有路径的 import 语句时,我们会为你提供可能的导入列表。
当你确认完成时,我们会补全完整的导入语句,包括你本来要写的路径。

🌐 So when you now start writing an import statement that doesn’t have a path, we’ll provide you with a list of possible imports. When you commit a completion, we’ll complete the full import statement, including the path that you were going to write.

Import statement completions

此功能需要专门支持该特性的编辑器。 你可以通过使用最新的 Visual Studio Code 内测版本 来尝试此功能。

🌐 This work requires editors that specifically support the feature. You’ll be able to try this out by using the latest Insiders versions of Visual Studio Code.

欲了解更多信息,请查看实现拉取请求

🌐 For more information, take a look at the implementing pull request!

🌐 Editor Support for @link Tags

TypeScript 现在可以理解 @link 标签,并会尝试解析它们链接到的声明。 这意味着你可以将鼠标悬停在 @link 标签中的名称上以快速获取信息,或者使用像“转到定义”或“查找所有引用”这样的命令。

🌐 TypeScript can now understand @link tags, and will try to resolve declarations that they link to. What this means is that you’ll be able to hover over names within @link tags and get quick information, or use commands like go-to-definition or find-all-references.

例如,你将能够在下面的示例中在 @link plantCarrot 中对 plantCarrot 使用“转到定义”,支持 TypeScript 的编辑器将跳转到 plantCarrot 的函数声明。

🌐 For example, you’ll be able to go-to-definition on plantCarrot in @link plantCarrot in the example below and a TypeScript-supported editor will jump to plantCarrot’s function declaration.

ts
/**
* To be called 70 to 80 days after {@link plantCarrot}.
*/
function harvestCarrot(carrot: Carrot) {}
/**
* Call early in spring for best results. Added in v2.1.0.
* @param seed Make sure it's a carrot seed!
*/
function plantCarrot(seed: Seed) {
// TODO: some gardening
}

Jumping to definition and requesting quick info on a @link tag for

欲了解更多信息,请参见 GitHub 上的拉取请求

🌐 For more information, see the pull request on GitHub!

非 JavaScript 文件路径的跳转到定义

🌐 Go-to-Definition on Non-JavaScript File Paths

许多加载器允许用户通过 JavaScript 导入在他们的应用中包含资源。它们通常会写成类似 import "./styles.css" 这样的形式。

🌐 Many loaders allow users to include assets in their applications using JavaScript imports. They’ll typically be written as something like import "./styles.css" or the like.

直到现在,TypeScript 的编辑器功能甚至不会尝试读取这个文件,因此“转到定义”通常会失败。最好的情况是,如果它能找到类似的内容,“转到定义”会跳转到像 declare module "*.css" 这样的声明处。

🌐 Up until now, TypeScript’s editor functionality wouldn’t even attempt to read this file, so go-to-definition would typically fail. At best, go-to-definition would jump to a declaration like declare module "*.css" if it could find something along those lines.

TypeScript 的语言服务现在在你对相对文件路径执行“转到定义”时,会尝试跳转到正确的文件,即使这些文件不是 JavaScript 或 TypeScript 文件! 试试对 CSS、SVG、PNG、字体文件、Vue 文件等的导入吧。

🌐 TypeScript’s language service now tries to jump to the correct file when you perform a go-to-definition on relative file paths, even if they’re not JavaScript or TypeScript files! Try it out with imports to CSS, SVGs, PNGs, font files, Vue files, and more.

欲了解更多信息,你可以查看实现的拉取请求

🌐 For more information, you can check out the implementing pull request.

打破变更

🌐 Breaking Changes

lib.d.ts 变更

🌐 lib.d.ts Changes

和每个 TypeScript 版本一样,lib.d.ts 的声明(尤其是为网页环境生成的声明)已经发生了变化。 在此版本中,我们利用了 Mozilla 的 browser-compat-data 来移除任何浏览器未实现的 API。 尽管你不太可能会使用它们,但像 AccountAssertionOptionsRTCStatsEventInitMSGestureEventDeviceLightEventMSPointerEventServiceWorkerMessageEventWebAuthentication 这样的 API 已经全部从 lib.d.ts 中移除。 这里有一些详细讨论

🌐 As with every TypeScript version, declarations for lib.d.ts (especially the declarations generated for web contexts), have changed. In this release, we leveraged Mozilla’s browser-compat-data to remove APIs that no browser implements. While it is unlike that you are using them, APIs such as Account, AssertionOptions, RTCStatsEventInit, MSGestureEvent, DeviceLightEvent, MSPointerEvent, ServiceWorkerMessageEvent, and WebAuthentication have all been removed from lib.d.ts. This is discussed in some detail here.

https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/991

useDefineForClassFields 现在在 esnext 上默认为 true,并最终将在 es2022 上默认设置为 true

🌐 useDefineForClassFields now defaults to true on esnext and eventually on es2022

在2021年,类字段特性被加入到JavaScript规范中,其行为与TypeScript的实现方式有所不同。为此,在TypeScript 3.7中,添加了一个标志(useDefineForClassFields)用于迁移生成的JavaScript,以符合JavaScript标准行为。

🌐 In 2021 the class fields feature was added into the JavaScript specification with behavior which differed from how TypeScript had implemented it. In preparation for this, in TypeScript 3.7, a flag was added (useDefineForClassFields) to migrate to emitted JavaScript to match the JavaScript standard behavior.

现在该功能已经在 JavaScript 中实现,我们将 ES2022 及更高版本(包括 ESNext)的默认值更改为 true

🌐 Now that the feature is in JavaScript we are changing the default to true for ES2022 and above, including ESNext.

关于 Always-Truthy Promise 的错误检查

🌐 Errors on Always-Truthy Promise Checks

根据 strictNullChecks 的规定,使用在条件检查中始终看起来已定义的 Promise 现在被视为错误。

🌐 Under strictNullChecks, using a Promise that always appears to be defined within a condition check is now considered an error.

ts
declare var p: Promise<number>;
if (p) {
// ~
// Error!
// This condition will always return true since
// this 'Promise<number>' appears to always be defined.
//
// Did you forget to use 'await'?
}

更多详情,请查看原始更改

🌐 For more details, see the original change.

联合枚举无法与任意数字进行比较

🌐 Union Enums Cannot Be Compared to Arbitrary Numbers

当某些 enum 的成员要么是自动填充的,要么是显而易见地写出的,它们被视为 联合 enum。在这些情况下,枚举可以回忆它可能表示的每个值。

🌐 Certain enums are considered union enums when their members are either automatically filled in, or trivially written. In those cases, an enum can recall each value that it potentially represents.

在 TypeScript 4.3 中,如果一个具有联合类型 enum 的值与一个它不可能相等的数字字面量进行比较,则类型检查器会发出错误。

🌐 In TypeScript 4.3, if a value with a union enum type is compared with a numeric literal that it could never be equal to, then the type-checker will issue an error.

ts
enum E {
A = 0,
B = 1,
}
function doSomething(x: E) {
// Error! This condition will always return 'false' since the types 'E' and '-1' have no overlap.
if (x === -1) {
// ...
}
}

作为一种解决方法,你可以重写注解以包含适当的字面量类型。

🌐 As a workaround, you can re-write an annotation to include the appropriate literal type.

ts
enum E {
A = 0,
B = 1,
}
// Include -1 in the type, if we're really certain that -1 can come through.
function doSomething(x: E | -1) {
if (x === -1) {
// ...
}
}

你还可以对值使用类型断言。

🌐 You can also use a type-assertion on the value.

ts
enum E {
A = 0,
B = 1,
}
function doSomething(x: E) {
// Use a type assertion on 'x' because we know we're not actually just dealing with values from 'E'.
if ((x as number) === -1) {
// ...
}
}

或者,你可以重新声明你的枚举,使其具有非平凡的初始化器,这样任何数字都可以被分配给该枚举并与之比较。如果枚举的目的是指定一些知名的值,这可能会很有用。

🌐 Alternatively, you can re-declare your enum to have a non-trivial initializer so that any number is both assignable and comparable to that enum. This may be useful if the intent is for the enum to specify a few well-known values.

ts
enum E {
// the leading + on 0 opts TypeScript out of inferring a union enum.
A = +0,
B = 1,
}

欲了解更多详情,请参阅原始更改

🌐 For more details, see the original change