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 将仅使用 “reading” 类型(例如,上面 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 中扩展类时,该语言使得方法覆盖变得非常容易(一语双关)。 - 但不幸的是,你可能会遇到一些错误。

¥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,并用一个方法替换它们,会发生什么?

¥SpecializedComponent subclasses SomeComponent, and overrides the show and hide methods. What happens if someone decides to rip out show and hide and replace them with a single method?

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 关键字,否则覆盖超类中的任何方法都会出错。在最后一个例子中,TypeScript 在 noImplicitOverride 下会出错,并提示我们可能需要在 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一个拉取请求 中实现的,尽管 Paul Cody Johnston 之前仅实现了 override 关键字的拉取请求作为指导和讨论的基础。我们非常感谢你为这些功能付出的时间。

¥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";

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

¥…or match patterns of other string-like types.

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 可以匹配字符串内容,并在第一次赋值时确定 s2s1 兼容;然而,一旦它看到另一个模板字符串,它就放弃了。因此,像 s3s1 这样的赋值操作根本不起作用。

¥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 的原始类型。我们可以通过在 Set<T> | T[] 的位置添加一个名为 C 的类型参数来实现这一点。

¥Let’s leave questions about this function’s implementation aside, and assume it arose from the requirements of a broader application. Something that you might notice is that the signature doesn’t capture the original type of collection. We can do that by adding a type parameter called C in place of where we’ve written 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

¥The issue is that when we perform our collection instanceof Set check, we’re expecting that to act as a type guard that narrows the type from Set<T> | T[] to Set<T> and T[] depending on the branch we’re in; however, we’re not dealing with a Set<T> | T[], we’re trying to narrow the generic value collection, whose type is C.

这是一个非常微妙的区别,但它确实很重要。TypeScript 不能仅仅抓住 C(即 Set<T> | T[])的约束并缩小其范围。如果 TypeScript 尝试从 Set<T> | T[] 缩小范围,它会忘记 collection 在每个分支中也是 C,因为没有简单的方法来保留该信息。如果 TypeScript 尝试这种方法,它将以不同的方式破坏上述示例。在返回位置,函数期望传入类型为 C 的值,但每个分支中我们得到的却是 Set<T>T[],而 TypeScript 会拒绝这些值。

¥It’s a very subtle distinction, but it makes a difference. TypeScript can’t just grab the constraint of C (which is Set<T> | T[]) and narrow that. If TypeScript did try to narrow from Set<T> | T[], it would forget that collection is also a C in each branch because there’s no easy way to preserve that information. If hypothetically TypeScript tried that approach, it would break the above example in a different way. At the return positions, where the function expects values with the type C, we would instead get a Set<T> and a T[] in each branch, which TypeScript would reject.

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 是否为 “truthy” 将触发错误。

¥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 贡献,我们向他们表示感谢!

¥This change was contributed by Jack Works, and we extend our thanks to them!

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

    ¥1MB to 411 KB

  • 14.9MB 到 1MB

    ¥14.9MB to 1MB

  • 1345MB 到 467MB

    ¥1345MB to 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 开发,其工作随后在 最终修改 中实现了此功能。我们要衷心感谢 Tobias 帮助我们找到这些改进机会!

¥This work was started by Tobias Koppers, whose work ensued in the resulting final change for this functionality. We’d like to extend a great thanks to Tobias for helping us find these opportunities for improvements!

导入语句补全

¥Import Statement Completions

用户在使用 JavaScript 中的 import 和 export 语句时遇到的最大痛点之一是顺序。 - 具体来说,导入语句应写为

¥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 Insiders 版本 来尝试一下。

¥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 标签内的名称上并快速获取信息,或者使用诸如 go-to-definition 或 find-all-references 之类的命令。

¥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 上使用跳转至定义 (go-to-definition),并且支持 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 的编辑器功能甚至不会尝试读取此文件,因此转到定义通常会失败。在最好的情况下,如果 go-to-definition 能找到类似的声明,它就会跳转到类似 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.

现在,当你在相对文件路径上执行 go-to-definition 时,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 的声明(尤其是为 Web 上下文生成的声明)都已更改。在此版本中,我们利用 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