TypeScript 5.0

装饰器

¥Decorators

装饰器是即将推出的 ECMAScript 功能,它允许我们以可重用的方式自定义类及其成员。

¥Decorators are an upcoming ECMAScript feature that allow us to customize classes and their members in a reusable way.

让我们考虑以下代码:

¥Let’s consider the following code:

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();

greet 在这里非常简单,但我们可以假设它要复杂得多。 - 它可能执行一些异步逻辑、递归、有副作用等等。无论你想象的是什么样的泥球,假设你加入一些 console.log 调用来帮助调试 greet

¥greet is pretty simple here, but let’s imagine it’s something way more complicated - maybe it does some async logic, it’s recursive, it has side effects, etc. Regardless of what kind of ball-of-mud you’re imagining, let’s say you throw in some console.log calls to help debug greet.

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}

这种模式相当常见。如果我们能为每个方法都这样做就太好了!

¥This pattern is fairly common. It sure would be nice if there was a way we could do this for every method!

装饰器就此登场。我们可以编写一个名为 loggedMethod 的函数,如下所示:

¥This is where decorators come in. We can write a function called loggedMethod that looks like the following:

ts
function loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}

“所有这些 any 是怎么回事?”这是什么,anyScript!?

¥“What’s the deal with all of these anys? What is this, anyScript!?”

请耐心等待 - 我们目前保持简单,以便专注于此函数的功能。请注意,loggedMethod 接受原始方法 (originalMethod) 并返回一个函数,该函数

¥Just be patient - we’re keeping things simple for now so that we can focus on what this function is doing. Notice that loggedMethod takes the original method (originalMethod) and returns a function that

  1. 记录一条 “正在输入…” 消息

    ¥logs an “Entering…” message

  2. this 及其所有参数传递给原始方法

    ¥passes along this and all of its arguments to the original method

  3. 记录 “正在退出…” 消息,并且

    ¥logs an “Exiting…” message, and

  4. 返回原始方法返回的内容。

    ¥returns whatever the original method returned.

现在我们可以使用 loggedMethod 来装饰方法 greet

¥Now we can use loggedMethod to decorate the method greet:

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
// Output:
//
// LOG: Entering method.
// Hello, my name is Ray.
// LOG: Exiting method.

我们只是将 loggedMethod 用作 greet 上的装饰器。 - 请注意,我们将其写为 @loggedMethod。当我们这样做时,它会使用方法目标和上下文对象进行调用。由于 loggedMethod 返回了一个新函数,该函数取代了 greet 的原始定义。

¥We just used loggedMethod as a decorator above greet - and notice that we wrote it as @loggedMethod. When we did that, it got called with the method target and a context object. Because loggedMethod returned a new function, that function replaced the original definition of greet.

我们还没有提到它,但 loggedMethod 是用第二个参数定义的。它被称为 “上下文对象”,它包含一些关于如何声明修饰方法的有用信息。 - 例如,它是 #private 成员还是 static,或者方法的名称是什么。让我们重写 loggedMethod 来利用这一点,并打印出被修饰方法的名称。

¥We didn’t mention it yet, but loggedMethod was defined with a second parameter. It’s called a “context object”, and it has some useful information about how the decorated method was declared - like whether it was a #private member, or static, or what the name of the method was. Let’s rewrite loggedMethod to take advantage of that and print out the name of the method that was decorated.

ts
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}

我们现在正在使用 context 参数。 - 并且它是 loggedMethod 中第一个类型比 anyany[] 更严格的对象。TypeScript 提供了一个名为 ClassMethodDecoratorContext 的类型,用于模拟方法装饰器所采用的上下文对象。

¥We’re now using the context parameter - and that it’s the first thing in loggedMethod that has a type stricter than any and any[]. TypeScript provides a type called ClassMethodDecoratorContext that models the context object that method decorators take.

除了元数据之外,方法的上下文对象还有一个名为 addInitializer 的实用函数。这是一种连接到构造函数开头的方法(如果我们使用的是 static,则连接到类本身的初始化)。

¥Apart from metadata, the context object for methods also has a useful function called addInitializer. It’s a way to hook into the beginning of the constructor (or the initialization of the class itself if we’re working with statics).

例如: - 在 JavaScript 中,通常使用类似以下格式的写法:

¥As an example - in JavaScript, it’s common to write something like the following pattern:

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}

或者,可以将 greet 声明为一个属性,并初始化为箭头函数。

¥Alternatively, greet might be declared as a property initialized to an arrow function.

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet = () => {
console.log(`Hello, my name is ${this.name}.`);
};
}

此代码旨在确保当 greet 作为独立函数调用或作为回调传递时,this 不会被重新绑定。

¥This code is written to ensure that this isn’t re-bound if greet is called as a stand-alone function or passed as a callback.

ts
const greet = new Person("Ray").greet;
// We don't want this to fail!
greet();

我们可以编写一个装饰器,使用 addInitializer 在构造函数中调用 bind

¥We can write a decorator that uses addInitializer to call bind in the constructor for us.

ts
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
if (context.private) {
throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}

bound 没有返回任何内容 - 因此,当它修饰方法时,不会影响原始方法。相反,它会在初始化任何其他字段之前添加逻辑。

¥bound isn’t returning anything - so when it decorates a method, it leaves the original alone. Instead, it will add logic before any other fields are initialized.

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
const greet = p.greet;
// Works!
greet();

请注意,我们堆叠了两个装饰器 - @bound@loggedMethod。这些修饰符在 “反向顺序” 中运行。也就是说,@loggedMethod 装饰了原始方法 greet,而 @bound 装饰了 @loggedMethod 的结果。在此示例中,这并不重要。 - 但是如果你的装饰器有副作用或需要特定的顺序,那么这种情况就可能发生。

¥Notice that we stacked two decorators - @bound and @loggedMethod. These decorations run in “reverse order”. That is, @loggedMethod decorates the original method greet, and @bound decorates the result of @loggedMethod. In this example, it doesn’t matter - but it could if your decorators have side-effects or expect a certain order.

同样值得注意的是: - 如果你更喜欢这种风格,可以将这些装饰器放在同一行。

¥Also worth noting - if you’d prefer stylistically, you can put these decorators on the same line.

ts
@bound @loggedMethod greet() {
console.log(`Hello, my name is ${this.name}.`);
}

可能不太明显的是,我们甚至可以创建返回装饰器函数的函数。这使得我们可以稍微自定义最终的装饰器。如果我们愿意,可以让 loggedMethod 返回一个装饰器,并自定义它如何记录消息。

¥Something that might not be obvious is that we can even make functions that return decorator functions. That makes it possible to customize the final decorator just a little. If we wanted, we could have made loggedMethod return a decorator and customize how it logs its messages.

ts
function loggedMethod(headMessage = "LOG:") {
return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`${headMessage} Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`${headMessage} Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
}

如果这样做,我们必须在将 loggedMethod 用作装饰器之前调用它。然后,我们可以将任何字符串作为前缀传递给控制台记录的消息。

¥If we did that, we’d have to call loggedMethod before using it as a decorator. We could then pass in any string as the prefix for messages that get logged to the console.

ts
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod("⚠️")
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
// Output:
//
// ⚠️ Entering method 'greet'.
// Hello, my name is Ray.
// ⚠️ Exiting method 'greet'.

装饰器不仅可以用于方法!它们可以用于属性/字段、getter、setter 和自动访问器。即使是类本身也可以被修饰,以便进行子类化和注册等操作。

¥Decorators can be used on more than just methods! They can be used on properties/fields, getters, setters, and auto-accessors. Even classes themselves can be decorated for things like subclassing and registration.

要深入了解装饰器,你可以阅读 Axel Rauschmayer 的详尽总结

¥To learn more about decorators in-depth, you can read up on Axel Rauschmayer’s extensive summary.

有关所涉及更改的更多信息,请参阅 查看原始拉取请求

¥For more information about the changes involved, you can view the original pull request.

与实验性旧式装饰器的区别

¥Differences with Experimental Legacy Decorators

如果你已经使用 TypeScript 一段时间,你可能已经注意到它多年来一直支持 “experimental” 装饰器。虽然这些实验性的装饰器非常有用,但它们模拟了装饰器提案的旧版本,并且始终需要一个名为 --experimentalDecorators 的选择加入编译器标志。任何在 TypeScript 中使用装饰器而不使用此标志的尝试都会提示错误消息。

¥If you’ve been using TypeScript for a while, you might be aware of the fact that it’s had support for “experimental” decorators for years. While these experimental decorators have been incredibly useful, they modeled a much older version of the decorators proposal, and always required an opt-in compiler flag called --experimentalDecorators. Any attempt to use decorators in TypeScript without this flag used to prompt an error message.

--experimentalDecorators 在可预见的未来将继续存在;但是,如果没有该标志,装饰器现在将成为所有新代码的有效语法。在 --experimentalDecorators 之外,它们将以不同的方式进行类型检查和触发。类型检查规则和触发有很大不同,虽然可以编写装饰器来支持新旧装饰器的行为,但任何现有的装饰器函数都不太可能这样做。

¥--experimentalDecorators will continue to exist for the foreseeable future; however, without the flag, decorators will now be valid syntax for all new code. Outside of --experimentalDecorators, they will be type-checked and emitted differently. The type-checking rules and emit are sufficiently different that while decorators can be written to support both the old and new decorators behavior, any existing decorator functions are not likely to do so.

此新的装饰器提案与 --emitDecoratorMetadata 不兼容,并且不允许装饰参数。未来的 ECMAScript 提案或许能够帮助弥合这一差距。

¥This new decorators proposal is not compatible with --emitDecoratorMetadata, and it does not allow decorating parameters. Future ECMAScript proposals may be able to help bridge that gap.

最后补充说明:除了允许将装饰器放在 export 关键字之前之外,装饰器提案现在还提供了将装饰器放在 exportexport default 之后的选项。唯一的例外是不允许混合使用这两种样式。

¥On a final note: in addition to allowing decorators to be placed before the export keyword, the proposal for decorators now provides the option of placing decorators after export or export default. The only exception is that mixing the two styles is not allowed.

js
// ✅ allowed
@register export default class Foo {
// ...
}
// ✅ also allowed
export default @register class Bar {
// ...
}
// ❌ error - before *and* after is not allowed
@before export @after class Bar {
// ...
}

编写类型良好的装饰器

¥Writing Well-Typed Decorators

上面的 loggedMethodbound 装饰器示例刻意简化,省略了许多关于类型的细节。

¥The loggedMethod and bound decorator examples above are intentionally simple and omit lots of details about types.

类型装饰器可能相当复杂。例如,上面 loggedMethod 的类型声明版本可能如下所示:

¥Typing decorators can be fairly complex. For example, a well-typed version of loggedMethod from above might look something like this:

ts
function loggedMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`LOG: Entering method '${methodName}'.`)
const result = target.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}

我们必须分别使用类型参数 ThisArgsReturn 来建模 this 的类型、参数以及原始方法的返回类型。

¥We had to separately model out the type of this, the parameters, and the return type of the original method, using the type parameters This, Args, and Return.

装饰器函数的具体复杂程度取决于你想要保证的内容。请记住,装饰器的使用频率会高于其编写频率,因此通常建议使用类型良好的版本。 - 但这显然会影响可读性,所以请尽量保持简洁。

¥Exactly how complex your decorators functions are defined depends on what you want to guarantee. Just keep in mind, your decorators will be used more than they’re written, so a well-typed version will usually be preferable - but there’s clearly a trade-off with readability, so try to keep things simple.

更多关于编写装饰器的文档将在未来提供。 - 但 此帖子 应该包含大量有关装饰器机制的细节。

¥More documentation on writing decorators will be available in the future - but this post should have a good amount of detail for the mechanics of decorators.

const 类型参数

¥const Type Parameters

在推断对象的类型时,TypeScript 通常会选择一种通用的类型。例如,在这种情况下,names 的推断类型是 string[]

¥When inferring the type of an object, TypeScript will usually choose a type that’s meant to be general. For example, in this case, the inferred type of names is string[]:

ts
type HasNames = { names: readonly string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
return arg.names;
}
// Inferred type: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});

通常,这样做的目的是为了便于后续修改。

¥Usually the intent of this is to enable mutation down the line.

然而,根据 getNamesExactly 的具体功能及其预期用途,通常可能需要更具体的类型。

¥However, depending on what exactly getNamesExactly does and how it’s intended to be used, it can often be the case that a more-specific type is desired.

到目前为止,API 作者通常必须建议在某些位置添加 as const 才能实现所需的推断:

¥Up until now, API authors have typically had to recommend adding as const in certain places to achieve the desired inference:

ts
// The type we wanted:
// readonly ["Alice", "Bob", "Eve"]
// The type we got:
// string[]
const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
// Correctly gets what we wanted:
// readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);

这可能很麻烦且容易忘记。在 TypeScript 5.0 中,现在可以在类型参数声明中添加 const 修饰符,以使类似 const 的推断成为默认值:

¥This can be cumbersome and easy to forget. In TypeScript 5.0, you can now add a const modifier to a type parameter declaration to cause const-like inference to be the default:

ts
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
// ^^^^^
return arg.names;
}
// Inferred type: readonly ["Alice", "Bob", "Eve"]
// Note: Didn't need to write 'as const' here
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

请注意,const 修饰符不会拒绝可变值,也不需要不可变约束。使用可变类型约束可能会产生令人惊讶的结果。例如:

¥Note that the const modifier doesn’t reject mutable values, and doesn’t require immutable constraints. Using a mutable type constraint might give surprising results. For example:

ts
declare function fnBad<const T extends string[]>(args: T): void;
// 'T' is still 'string[]' since 'readonly ["a", "b", "c"]' is not assignable to 'string[]'
fnBad(["a", "b" ,"c"]);

这里,T 的推断候选对象是 readonly ["a", "b", "c"],而 readonly 数组不能用于需要可变数组的地方。在这种情况下,推断会回退到约束条件,数组将被视为 string[],调用仍然会成功进行。

¥Here, the inferred candidate for T is readonly ["a", "b", "c"], and a readonly array can’t be used where a mutable one is needed. In this case, inference falls back to the constraint, the array is treated as string[], and the call still proceeds successfully.

此函数的更好定义应该使用 readonly string[]

¥A better definition of this function should use readonly string[]:

ts
declare function fnGood<const T extends readonly string[]>(args: T): void;
// T is readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);

同样,请记住 const 修饰符仅影响调用中编写的对象、数组和原始表达式的推断,因此无法使用 as const 修改的参数不会在行为上发生任何变化:

¥Similarly, remember to keep in mind that the const modifier only affects inference of object, array and primitive expressions that were written within the call, so arguments which wouldn’t (or couldn’t) be modified with as const won’t see any change in behavior:

ts
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ["a", "b" ,"c"];
// 'T' is still 'string[]'-- the 'const' modifier has no effect here
fnGood(arr);

有关更多详细信息,请参阅 查看拉取请求 以及(firstsecond)的激励问题。

¥See the pull request and the (first and second) motivating issues for more details.

extends 中支持多个配置文件

¥Supporting Multiple Configuration Files in extends

在管理多个项目时,拥有一个可供其他 tsconfig.json 文件扩展的 “base” 配置文件会很有帮助。这就是 TypeScript 支持使用 extends 字段从 compilerOptions 复制字段的原因。

¥When managing multiple projects, it can be helpful to have a “base” configuration file that other tsconfig.json files can extend from. That’s why TypeScript supports an extends field for copying over fields from compilerOptions.

jsonc
// packages/front-end/src/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib",
// ...
}
}

但是,在某些情况下,你可能需要从多个配置文件进行扩展。例如,假设使用 已发布到 npm 的 TypeScript 基础配置文件。如果你希望所有项目也使用 npm 上 @tsconfig/strictest 包中的选项,那么有一个简单的解决方案:tsconfig.base.json 继承自 @tsconfig/strictest

¥However, there are scenarios where you might want to extend from multiple configuration files. For example, imagine using a TypeScript base configuration file shipped to npm. If you want all your projects to also use the options from the @tsconfig/strictest package on npm, then there’s a simple solution: have tsconfig.base.json extend from @tsconfig/strictest:

jsonc
// tsconfig.base.json
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
// ...
}
}

这在一定程度上是可行的。如果你有任何不想使用 @tsconfig/strictest 的项目,他们必须手动禁用选项,或者创建一个不从 @tsconfig/strictest 扩展的单独版本的 tsconfig.base.json

¥This works to a point. If you have any projects that don’t want to use @tsconfig/strictest, they have to either manually disable the options, or create a separate version of tsconfig.base.json that doesn’t extend from @tsconfig/strictest.

为了提供更多灵活性,TypeScript 5.0 现在允许 extends 字段接受多个条目。例如,在此配置文件中:

¥To give some more flexibility here, Typescript 5.0 now allows the extends field to take multiple entries. For example, in this configuration file:

jsonc
{
"extends": ["a", "b", "c"],
"compilerOptions": {
// ...
}
}

编写这样的代码有点像直接扩展 c,其中 c 扩展 bb 扩展 a。如果 “conflict” 有任何字段,则后者生效。

¥Writing this is kind of like extending c directly, where c extends b, and b extends a. If any fields “conflict”, the latter entry wins.

因此在以下示例中,strictNullChecksnoImplicitAny 在最终的 tsconfig.json 中均启用。

¥So in the following example, both strictNullChecks and noImplicitAny are enabled in the final tsconfig.json.

jsonc
// tsconfig1.json
{
"compilerOptions": {
"strictNullChecks": true
}
}
// tsconfig2.json
{
"compilerOptions": {
"noImplicitAny": true
}
}
// tsconfig.json
{
"extends": ["./tsconfig1.json", "./tsconfig2.json"],
"files": ["./index.ts"]
}

再例如,我们可以按以下方式重写原始示例。

¥As another example, we can rewrite our original example in the following way.

jsonc
// packages/front-end/src/tsconfig.json
{
"extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],
"compilerOptions": {
"outDir": "../lib",
// ...
}
}

详情请见 在原始拉取请求中阅读更多内容

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

所有 enum 都是 Union enum

¥All enums Are Union enums

TypeScript 最初引入枚举时,它们只不过是一组具有相同类型的数字常量。

¥When TypeScript originally introduced enums, they were nothing more than a set of numeric constants with the same type.

ts
enum E {
Foo = 10,
Bar = 20,
}

E.FooE.Bar 的唯一特殊之处在于它们可以赋值给任何期望类型 E 的对象。除此之外,它们实际上就是 number

¥The only thing special about E.Foo and E.Bar was that they were assignable to anything expecting the type E. Other than that, they were pretty much just numbers.

ts
function takeValue(e: E) {}
takeValue(E.Foo); // works
takeValue(123); // error!

直到 TypeScript 2.0 引入枚举字面量类型,枚举才变得更加特殊。枚举字面量类型为每个枚举成员赋予其自己的类型,并将枚举本身转换为每个成员类型的联合。它们还允许我们仅引用枚举类型的子集,并缩小这些类型的范围。

¥It wasn’t until TypeScript 2.0 introduced enum literal types that enums got a bit more special. Enum literal types gave each enum member its own type, and turned the enum itself into a union of each member type. They also allowed us to refer to only a subset of the types of an enum, and to narrow away those types.

ts
// Color is like a union of Red | Orange | Yellow | Green | Blue | Violet
enum Color {
Red, Orange, Yellow, Green, Blue, /* Indigo, */ Violet
}
// Each enum member has its own type that we can refer to!
type PrimaryColor = Color.Red | Color.Green | Color.Blue;
function isPrimaryColor(c: Color): c is PrimaryColor {
// Narrowing literal types can catch bugs.
// TypeScript will error here because
// we'll end up comparing 'Color.Red' to 'Color.Green'.
// We meant to use ||, but accidentally wrote &&.
return c === Color.Red && c === Color.Green && c === Color.Blue;
}

为每个枚举成员赋予其自己的类型的一个问题是,这些类型在某种程度上与成员的实际值相关联。在某些情况下,无法计算该值。 - 例如,枚举成员可以通过函数调用初始化。

¥One issue with giving each enum member its own type was that those types were in some part associated with the actual value of the member. In some cases it’s not possible to compute that value - for instance, an enum member could be initialized by a function call.

ts
enum E {
Blah = Math.random()
}

每当 TypeScript 遇到这些问题时,它都会悄悄地退出并使用旧的枚举策略。这意味着放弃联合体和字面量类型的所有优势。

¥Whenever TypeScript ran into these issues, it would quietly back out and use the old enum strategy. That meant giving up all the advantages of unions and literal types.

TypeScript 5.0 通过为每个计算成员创建唯一类型,将所有枚举转换为联合枚举。这意味着现在所有枚举都可以被缩小,并且其成员也可以作为类型引用。

¥TypeScript 5.0 manages to make all enums into union enums by creating a unique type for each computed member. That means that all enums can now be narrowed and have their members referenced as types as well.

更多变更详情,请访问 在 GitHub 上阅读具体信息

¥For more details on this change, you can read the specifics on GitHub.

--moduleResolution bundler

TypeScript 4.7 为其 --module--moduleResolution 设置引入了 node16nodenext 选项。这些选项的目的是更好地模拟 Node.js 中 ECMAScript 模块的精确查找规则;然而,此模式有许多限制,其他工具实际上并不强制执行。

¥TypeScript 4.7 introduced the node16 and nodenext options for its --module and --moduleResolution settings. The intent of these options was to better model the precise lookup rules for ECMAScript modules in Node.js; however, this mode has many restrictions that other tools don’t really enforce.

例如,在 Node.js 的 ECMAScript 模块中,任何相对导入都需要包含文件扩展名。

¥For example, in an ECMAScript module in Node.js, any relative import needs to include a file extension.

js
// entry.mjs
import * as utils from "./utils"; // ❌ wrong - we need to include the file extension.
import * as utils from "./utils.mjs"; // ✅ works

Node.js 和浏览器中存在某些原因。 - 它使文件查找更快,并且更适合简单的文件服务器。但对于许多使用打包器等工具的开发者来说,node16/nodenext 设置很麻烦,因为打包器没有大多数此类限制。在某些方面,node 解析模式对于使用打包器的任何人来说都更好。

¥There are certain reasons for this in Node.js and the browser - it makes file lookups faster and works better for naive file servers. But for many developers using tools like bundlers, the node16/nodenext settings were cumbersome because bundlers don’t have most of these restrictions. In some ways, the node resolution mode was better for anyone using a bundler.

但在某些方面,原始的 node 解析模式已经过时了。大多数现代打包器在 Node.js 中融合了 ECMAScript 模块和 CommonJS 查找规则。例如,无扩展名导入就像在 CommonJS 中一样正常工作,但在查看包的 export 条件 时,它们会像在 ECMAScript 文件中一样优先使用 import 条件。

¥But in some ways, the original node resolution mode was already out of date. Most modern bundlers use a fusion of the ECMAScript module and CommonJS lookup rules in Node.js. For example, extensionless imports work just fine just like in CommonJS, but when looking through the export conditions of a package, they’ll prefer an import condition just like in an ECMAScript file.

为了模拟打包器的工作方式,TypeScript 现在引入了一种新策略:--moduleResolution bundler

¥To model how bundlers work, TypeScript now introduces a new strategy: --moduleResolution bundler.

jsonc
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "bundler"
}
}

如果你使用的是 Vite、esbuild、swc、Webpack、Parcel 等实现混合查找策略的现代打包工具,那么新的 bundler 选项应该非常适合你。

¥If you are using a modern bundler like Vite, esbuild, swc, Webpack, Parcel, and others that implement a hybrid lookup strategy, the new bundler option should be a good fit for you.

另一方面,如果你正在编写一个打算发布到 npm 的库,使用 bundler 选项可以隐藏可能对不使用打包器的用户出现的兼容性问题。因此在这些情况下,使用 node16nodenext 解析选项可能是更好的选择。

¥On the other hand, if you’re writing a library that’s meant to be published on npm, using the bundler option can hide compatibility issues that may arise for your users who aren’t using a bundler. So in these cases, using the node16 or nodenext resolution options is likely to be a better path.

了解更多关于 --moduleResolution bundler查看实现拉取请求 的信息。

¥To read more on --moduleResolution bundler, take a look at the implementing pull request.

解析自定义标志

¥Resolution Customization Flags

JavaScript 工具现在可以模拟 “hybrid” 解析规则,就像我们上面描述的 bundler 模式一样。由于不同工具的支持略有不同,TypeScript 5.0 提供了启用或禁用一些功能的方法,这些功能可能适用于你的配置,也可能不适用于你的配置。

¥JavaScript tooling may now model “hybrid” resolution rules, like in the bundler mode we described above. Because tools may differ in their support slightly, TypeScript 5.0 provides ways to enable or disable a few features that may or may not work with your configuration.

allowImportingTsExtensions

--allowImportingTsExtensions 允许 TypeScript 文件使用 TypeScript 特定的扩展(如 .ts.mts.tsx)相互导入。

¥--allowImportingTsExtensions allows TypeScript files to import each other with a TypeScript-specific extension like .ts, .mts, or .tsx.

仅当启用 --noEmit--emitDeclarationOnly 时才允许使用此标志,因为这些导入路径在 JavaScript 输出文件中运行时无法解析。这里的期望是你的解析器(例如你的打包器、运行时或其他工具)将使 .ts 文件之间的这些导入工作。

¥This flag is only allowed when --noEmit or --emitDeclarationOnly is enabled, since these import paths would not be resolvable at runtime in JavaScript output files. The expectation here is that your resolver (e.g. your bundler, a runtime, or some other tool) is going to make these imports between .ts files work.

resolvePackageJsonExports

如果 TypeScript 从 node_modules 中的包中读取,--resolvePackageJsonExports 会强制 TypeScript 咨询 package.json 文件的 exports 字段

¥--resolvePackageJsonExports forces TypeScript to consult the exports field of package.json files if it ever reads from a package in node_modules.

对于 --moduleResolution,此选项在 node16nodenextbundler 选项下默认为 true

¥This option defaults to true under the node16, nodenext, and bundler options for --moduleResolution.

resolvePackageJsonImports

--resolvePackageJsonImports 强制 TypeScript 在从祖级目录包含 package.json 的文件中执行以 # 开头的查找时咨询 package.json 文件的 imports 字段

¥--resolvePackageJsonImports forces TypeScript to consult the imports field of package.json files when performing a lookup that starts with # from a file whose ancestor directory contains a package.json.

对于 --moduleResolution,此选项在 node16nodenextbundler 选项下默认为 true

¥This option defaults to true under the node16, nodenext, and bundler options for --moduleResolution.

allowArbitraryExtensions

在 TypeScript 5.0 中,当导入路径以未知的 JavaScript 或 TypeScript 文件扩展名结尾时,编译器将以 {file basename}.d.{extension}.ts 的形式查找该路径的声明文件。例如,如果你在打包器项目中使用 CSS 加载器,则可能需要为这些样式表编写(或生成)声明文件:

¥In TypeScript 5.0, when an import path ends in an extension that isn’t a known JavaScript or TypeScript file extension, the compiler will look for a declaration file for that path in the form of {file basename}.d.{extension}.ts. For example, if you are using a CSS loader in a bundler project, you might want to write (or generate) declaration files for those stylesheets:

css
/* app.css */
.cookie-banner {
display: none;
}
ts
// app.d.css.ts
declare const css: {
cookieBanner: string;
};
export default css;
ts
// App.tsx
import styles from "./app.css";
styles.cookieBanner; // string

默认情况下,此导入将引发错误,让你知道 TypeScript 不理解此文件类型,并且你的运行时可能不支持导入它。但是,如果你已配置运行时或打包程序来处理它,则可以使用新的 --allowArbitraryExtensions 编译器选项抑制错误。

¥By default, this import will raise an error to let you know that TypeScript doesn’t understand this file type and your runtime might not support importing it. But if you’ve configured your runtime or bundler to handle it, you can suppress the error with the new --allowArbitraryExtensions compiler option.

请注意,从历史上看,通过添加名为 app.css.d.ts 而不是 app.d.css.ts 的声明文件通常可以实现类似的效果 - 但是,这仅通过 Node 的 CommonJS require 解析规则起作用。严格来说,前者被解释为名为 app.css.js 的 JavaScript 文件的声明文件。因为相对文件导入需要在 Node 的 ESM 支持中包含扩展,所以 TypeScript 会在 --moduleResolution node16nodenext 下的 ESM 文件中对我们的示例出错。

¥Note that historically, a similar effect has often been achievable by adding a declaration file named app.css.d.ts instead of app.d.css.ts - however, this just worked through Node’s require resolution rules for CommonJS. Strictly speaking, the former is interpreted as a declaration file for a JavaScript file named app.css.js. Because relative files imports need to include extensions in Node’s ESM support, TypeScript would error on our example in an ESM file under --moduleResolution node16 or nodenext.

有关更多信息,请阅读 此功能的提议其对应的拉取请求

¥For more information, read up the proposal for this feature and its corresponding pull request.

customConditions

--customConditions 获取一个额外的 conditions 列表,当 TypeScript 从 package.jsonexportsimports 字段解析时,这些列表应该会成功。这些条件会添加到解析器默认使用的任何现有条件中。

¥--customConditions takes a list of additional conditions that should succeed when TypeScript resolves from an exports or imports field of a package.json. These conditions are added to whatever existing conditions a resolver will use by default.

例如,当此字段在 tsconfig.json 中设置如下时:

¥For example, when this field is set in a tsconfig.json as so:

jsonc
{
"compilerOptions": {
"target": "es2022",
"moduleResolution": "bundler",
"customConditions": ["my-condition"]
}
}

任何时候在 package.json 中引用 exportsimports 字段,TypeScript 都会考虑称为 my-condition 的条件。

¥Any time an exports or imports field is referenced in package.json, TypeScript will consider conditions called my-condition.

因此,从具有以下 package.json 的包导入时

¥So when importing from a package with the following package.json

jsonc
{
// ...
"exports": {
".": {
"my-condition": "./foo.mjs",
"node": "./bar.mjs",
"import": "./baz.mjs",
"require": "./biz.mjs"
}
}
}

TypeScript 将尝试查找与 foo.mjs 对应的文件。

¥TypeScript will try to look for files corresponding to foo.mjs.

此字段仅在 --moduleResolutionnode16nodenextbundler 选项下有效。

¥This field is only valid under the node16, nodenext, and bundler options for --moduleResolution

--verbatimModuleSyntax

默认情况下,TypeScript 会执行称为导入省略的操作。基本上,如果你写类似的东西

¥By default, TypeScript does something called import elision. Basically, if you write something like

ts
import { Car } from "./car";
export function drive(car: Car) {
// ...
}

TypeScript 检测到你仅对类型使用导入并完全删除导入。你的输出 JavaScript 可能看起来像这样:

¥TypeScript detects that you’re only using an import for types and drops the import entirely. Your output JavaScript might look something like this:

js
export function drive(car) {
// ...
}

大多数情况下这很好,因为如果 Car 不是从 ./car 导出的值,我们会收到运行时错误。

¥Most of the time this is good, because if Car isn’t a value that’s exported from ./car, we’ll get a runtime error.

但它确实为某些边缘情况增加了一层复杂性。例如,请注意没有像 import "./car"; 这样的语句 - 导入已完全删除。这实际上对具有副作用或没有副作用的模块产生了影响。

¥But it does add a layer of complexity for certain edge cases. For example, notice there’s no statement like import "./car"; - the import was dropped entirely. That actually makes a difference for modules that have side-effects or not.

TypeScript 对 JavaScript 的 emit 策略还有另外几层复杂性 - 导入省略并不总是由导入的使用方式驱动 - 它通常也会参考值的声明方式。因此,并不总是清楚以下代码是否正确

¥TypeScript’s emit strategy for JavaScript also has another few layers of complexity - import elision isn’t always just driven by how an import is used - it often consults how a value is declared as well. So it’s not always clear whether code like the following

ts
export { Car } from "./car";

应该保留或删除。如果 Car 声明为类似 class 的内容,则可以将其保留在生成的 JavaScript 文件中。但是,如果 Car 仅声明为 type 别名或 interface,则 JavaScript 文件根本不应该导出 Car

¥should be preserved or dropped. If Car is declared with something like a class, then it can be preserved in the resulting JavaScript file. But if Car is only declared as a type alias or interface, then the JavaScript file shouldn’t export Car at all.

虽然 TypeScript 可能能够根据来自各个文件的信息做出这些触发决定,但并非每个编译器都可以。

¥While TypeScript might be able to make these emit decisions based on information from across files, not every compiler can.

导入和导出上的 type 修饰符对这些情况有些帮助。我们可以明确说明导入或导出是否仅用于类型分析,并且可以通过使用 type 修饰符将其完全放在 JavaScript 文件中。

¥The type modifier on imports and exports helps with these situations a bit. We can make it explicit whether an import or export is only being used for type analysis, and can be dropped entirely in JavaScript files by using the type modifier.

ts
// This statement can be dropped entirely in JS output
import type * as car from "./car";
// The named import/export 'Car' can be dropped in JS output
import { type Car } from "./car";
export { type Car } from "./car";

type 修饰符本身并不是很有用 - 默认情况下,模块省略仍将删除导入,并且没有什么强迫你区分 type 和普通导入和导出。因此,TypeScript 具有标志 --importsNotUsedAsValues 以确保你使用 type 修饰符,--preserveValueImports 以防止某些模块省略行为,--isolatedModules 以确保你的 TypeScript 代码可以在不同的编译器上运行。不幸的是,理解这 3 个标志的细节很难,并且仍然存在一些具有意外行为的边缘情况。

¥type modifiers are not quite useful on their own - by default, module elision will still drop imports, and nothing forces you to make the distinction between type and plain imports and exports. So TypeScript has the flag --importsNotUsedAsValues to make sure you use the type modifier, --preserveValueImports to prevent some module elision behavior, and --isolatedModules to make sure that your TypeScript code works across different compilers. Unfortunately, understanding the fine details of those 3 flags is hard, and there are still some edge cases with unexpected behavior.

TypeScript 5.0 引入了一个名为 --verbatimModuleSyntax 的新选项来简化情况。规则要简单得多 - 任何没有 type 修饰符的导入或导出都会保留。任何使用 type 修饰符的内容都会被完全删除。

¥TypeScript 5.0 introduces a new option called --verbatimModuleSyntax to simplify the situation. The rules are much simpler - any imports or exports without a type modifier are left around. Anything that uses the type modifier is dropped entirely.

ts
// Erased away entirely.
import type { A } from "a";
// Rewritten to 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";
// Rewritten to 'import {} from "xyz";'
import { type xyz } from "xyz";

有了这个新选项,所见即所得。

¥With this new option, what you see is what you get.

不过,这在模块互操作方面确实有一些影响。在此标志下,当你的设置或文件扩展名暗示不同的模块系统时,ECMAScript importexport 不会被重写为 require 调用。相反,你会收到一个错误。如果你需要触发使用 requiremodule.exports 的代码,则必须使用早于 ES2015 的 TypeScript 模块语法:

¥That does have some implications when it comes to module interop though. Under this flag, ECMAScript imports and exports won’t be rewritten to require calls when your settings or file extension implied a different module system. Instead, you’ll get an error. If you need to emit code that uses require and module.exports, you’ll have to use TypeScript’s module syntax that predates ES2015:

输入 TypeScript 输出 JavaScript
ts
import foo = require("foo");
js
const foo = require("foo");
ts
function foo() {}
function bar() {}
function baz() {}
export = {
foo,
bar,
baz
};
js
function foo() {}
function bar() {}
function baz() {}
module.exports = {
foo,
bar,
baz
};

虽然这是一个限制,但它确实有助于使一些问题更加明显。例如,忘记在 --module node16 下设置 package.json 中的 type 字段 是很常见的。因此,开发者会在不知不觉中开始编写 CommonJS 模块而不是 ES 模块,从而产生令人惊讶的查找规则和 JavaScript 输出。这个新标志确保你有意使用文件类型,因为语法是故意不同的。

¥While this is a limitation, it does help make some issues more obvious. For example, it’s very common to forget to set the type field in package.json under --module node16. As a result, developers would start writing CommonJS modules instead of ES modules without realizing it, giving surprising lookup rules and JavaScript output. This new flag ensures that you’re intentional about the file type you’re using because the syntax is intentionally different.

因为 --verbatimModuleSyntax 提供了比 --importsNotUsedAsValues--preserveValueImports 更一致的故事,所以这两个现有标志被弃用以支持它。

¥Because --verbatimModuleSyntax provides a more consistent story than --importsNotUsedAsValues and --preserveValueImports, those two existing flags are being deprecated in its favor.

更多详情,请参阅 [原始拉取请求]https://github.com/microsoft/TypeScript/pull/52203其提案问题

¥For more details, read up on [the original pull request]https://github.com/microsoft/TypeScript/pull/52203 and its proposal issue.

支持 export type *

¥Support for export type *

当 TypeScript 3.8 引入仅类型导入时,新语法不允许在 export * from "module"export * as ns from "module" 重新导出中使用。TypeScript 5.0 增加了对这两种形式的支持:

¥When TypeScript 3.8 introduced type-only imports, the new syntax wasn’t allowed on export * from "module" or export * as ns from "module" re-exports. TypeScript 5.0 adds support for both of these forms:

ts
// models/vehicles.ts
export class Spaceship {
// ...
}
// models/index.ts
export type * as vehicles from "./vehicles";
// main.ts
import { vehicles } from "./models";
function takeASpaceship(s: vehicles.Spaceship) {
// ✅ ok - `vehicles` only used in a type position
}
function makeASpaceship() {
return new vehicles.Spaceship();
// ^^^^^^^^
// 'vehicles' cannot be used as a value because it was exported using 'export type'.
}

你可以 在此处阅读有关实现的更多信息

¥You can read more about the implementation here.

@satisfies JSDoc 支持

¥@satisfies Support in JSDoc

TypeScript 4.9 引入了 satisfies 运算符。它确保了表达式的类型兼容,而不会影响类型本身。例如,让我们来看以下代码:

¥TypeScript 4.9 introduced the satisfies operator. It made sure that the type of an expression was compatible, without affecting the type itself. For example, let’s take the following code:

ts
interface CompilerOptions {
strict?: boolean;
outDir?: string;
// ...
}
interface ConfigSettings {
compilerOptions?: CompilerOptions;
extends?: string | string[];
// ...
}
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
// ...
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
} satisfies ConfigSettings;

这里,TypeScript 知道 myConfigSettings.extends 是用数组声明的。 - 因为虽然 satisfies 验证了对象的类型,但它并没有直接将其更改为 CompilerOptions 并丢失信息。如果我们想映射 extends,那没问题。

¥Here, TypeScript knows that myConfigSettings.extends was declared with an array - because while satisfies validated the type of our object, it didn’t bluntly change it to CompilerOptions and lose information. So if we want to map over extends, that’s fine.

ts
declare function resolveConfig(configPath: string): CompilerOptions;
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

这对 TypeScript 用户很有帮助,但很多人使用 TypeScript 通过 JSDoc 注释对其 JavaScript 代码进行类型检查。这就是为什么 TypeScript 5.0 支持一个名为 @satisfies 的新 JSDoc 标签,该标签的功能完全相同。

¥This was helpful for TypeScript users, but plenty of people use TypeScript to type-check their JavaScript code using JSDoc annotations. That’s why TypeScript 5.0 is supporting a new JSDoc tag called @satisfies that does exactly the same thing.

/** @satisfies */ 可以捕获类型不匹配:

¥/** @satisfies */ can catch type mismatches:

js
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @satisfies {CompilerOptions}
*/
let myCompilerOptions = {
outdir: "../lib",
// ~~~~~~ oops! we meant outDir
};

但它将保留表达式的原始类型,使我们能够在以后的代码中更精确地使用我们的值。

¥But it will preserve the original type of our expressions, allowing us to use our values more precisely later on in our code.

js
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @typedef ConfigSettings
* @prop {CompilerOptions} [compilerOptions]
* @prop {string | string[]} [extends]
*/
/**
* @satisfies {ConfigSettings}
*/
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
};
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);

/** @satisfies */ 也可以在任何带括号的表达式中内联使用。我们可以将 myCompilerOptions 写成如下形式:

¥/** @satisfies */ can also be used inline on any parenthesized expression. We could have written myCompilerOptions like this:

ts
let myConfigSettings = /** @satisfies {ConfigSettings} */ ({
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
});

为什么?通常,当你深入研究其他代码(例如函数调用)时,这样做更有意义。

¥Why? Well, it usually makes more sense when you’re deeper in some other code, like a function call.

js
compileCode(/** @satisfies {CompilerOptions} */ ({
// ...
}));

此功能Oleksandr Tarasiuk 提供!

¥This feature was provided thanks to Oleksandr Tarasiuk!

@overload JSDoc 支持

¥@overload Support in JSDoc

在 TypeScript 中,可以为函数指定重载。重载让我们能够使用不同的参数调用函数,并可能返回不同的结果。它们可以限制调用者实际使用我们函数的方式,并优化它们返回的结果。

¥In TypeScript, you can specify overloads for a function. Overloads give us a way to say that a function can be called with different arguments, and possibly return different results. They can restrict how callers can actually use our functions, and refine what results they’ll get back.

ts
// Our overloads:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;
// Our implementation:
function printValue(value: string | number, maximumFractionDigits?: number) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}

这里,我们提到 printValue 的第一个参数要么是 string,要么是 number。如果它接受 number,它可以接受第二个参数来确定我们可以打印多少位小数。

¥Here, we’ve said that printValue takes either a string or a number as its first argument. If it takes a number, it can take a second argument to determine how many fractional digits we can print.

TypeScript 5.0 现在允许 JSDoc 使用新的 @overload 标签声明重载。每个带有 @overload 标签的 JSDoc 注释都被视为以下函数声明的不同重载。

¥TypeScript 5.0 now allows JSDoc to declare overloads with a new @overload tag. Each JSDoc comment with an @overload tag is treated as a distinct overload for the following function declaration.

js
// @ts-check
/**
* @overload
* @param {string} value
* @return {void}
*/
/**
* @overload
* @param {number} value
* @param {number} [maximumFractionDigits]
* @return {void}
*/
/**
* @param {string | number} value
* @param {number} [maximumFractionDigits]
*/
function printValue(value, maximumFractionDigits) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}

现在,无论我们是在 TypeScript 还是 JavaScript 文件中编写,TypeScript 都会让我们知道我们是否错误地调用了函数。

¥Now regardless of whether we’re writing in a TypeScript or JavaScript file, TypeScript can let us know if we’ve called our functions incorrectly.

ts
// all allowed
printValue("hello!");
printValue(123.45);
printValue(123.45, 2);
printValue("hello!", 123); // error!

得益于 Tomasz Lenarcik,此新标签 已实现 也应运而生。

¥This new tag was implemented thanks to Tomasz Lenarcik.

--build 下传递特定于触发的标志

¥Passing Emit-Specific Flags Under --build

TypeScript 现在允许在 --build 模式下传递以下标志

¥TypeScript now allows the following flags to be passed under --build mode

  • --declaration

  • --emitDeclarationOnly

  • --declarationMap

  • --sourceMap

  • --inlineSourceMap

这使得在开发和生产构建过程中自定义构建的某些部分变得更加容易。

¥This makes it way easier to customize certain parts of a build where you might have different development and production builds.

例如,库的开发版本可能不需要生成声明文件,但生产版本需要。项目可以将声明触发配置为默认关闭,并且只需使用以下方式构建即可。

¥For example, a development build of a library might not need to produce declaration files, but a production build would. A project can configure declaration emit to be off by default and simply be built with

sh
tsc --build -p ./my-project-dir

完成内循环迭代后,“production” 构建只需传递 --declaration 标志即可。

¥Once you’re done iterating in the inner loop, a “production” build can just pass the --declaration flag.

sh
tsc --build -p ./my-project-dir --declaration

更多关于此变更的信息可在此处找到

¥More information on this change is available here.

编辑器中不区分大小写的导入排序

¥Case-Insensitive Import Sorting in Editors

在 Visual Studio 和 VS Code 等编辑器中,TypeScript 增强了组织和排序导入和导出的体验。不过,对于列表何时是 “sorted”,可能会有不同的解释。

¥In editors like Visual Studio and VS Code, TypeScript powers the experience for organizing and sorting imports and exports. Often though, there can be different interpretations of when a list is “sorted”.

例如,以下导入列表是否已排序?

¥For example, is the following import list sorted?

ts
import {
Toggle,
freeze,
toBoolean,
} from "./utils";

答案可能出人意料地是 “这取决于”。如果我们不关心大小写敏感性,那么这个列表显然没有排序。字母 f 位于 tT 之前。

¥The answer might surprisingly be “it depends”. If we don’t care about case-sensitivity, then this list is clearly not sorted. The letter f comes before both t and T.

但在大多数编程语言中,排序默认为比较字符串的字节值。JavaScript 比较字符串的方式意味着 "Toggle" 始终位于 "freeze" 之前,因为根据 ASCII 字符编码,大写字母位于小写字母之前。从这个角度来看,导入列表是有序的。

¥But in most programming languages, sorting defaults to comparing the byte values of strings. The way JavaScript compares strings means that "Toggle" always comes before "freeze" because according to the ASCII character encoding, uppercase letters come before lowercase. So from that perspective, the import list is sorted.

TypeScript 之前认为导入列表是有序的,因为它执行的是基本的区分大小写的排序。对于喜欢不区分大小写排序的开发者,或者使用 ESLint 等默认要求不区分大小写排序的工具的开发者来说,这可能会令人沮丧。

¥TypeScript previously considered the import list to be sorted because it was doing a basic case-sensitive sort. This could be a point of frustration for developers who preferred a case-insensitive ordering, or who used tools like ESLint which require case-insensitive ordering by default.

TypeScript 现在默认检测大小写敏感。这意味着 TypeScript 和 ESLint 等工具通常不会就如何对导入进行最佳排序相互 “fight”。

¥TypeScript now detects case sensitivity by default. This means that TypeScript and tools like ESLint typically won’t “fight” each other over how to best sort imports.

我们的团队也一直在试验 你可以在此处阅读更多排序策略。这些选项最终可能会由编辑器配置。目前,它们仍然不稳定且处于实验阶段,你现在可以在 VS Code 中使用 JSON 选项中的 typescript.unstable 条目来选择启用它们。以下是你可以尝试的所有选项(设置为默认值):

¥Our team has also been experimenting with further sorting strategies which you can read about here. These options may eventually be configurable by editors. For now, they are still unstable and experimental, and you can opt into them in VS Code today by using the typescript.unstable entry in your JSON options. Below are all of the options you can try out (set to their defaults):

jsonc
{
"typescript.unstable": {
// Should sorting be case-sensitive? Can be:
// - true
// - false
// - "auto" (auto-detect)
"organizeImportsIgnoreCase": "auto",
// Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
// - "ordinal"
// - "unicode"
"organizeImportsCollation": "ordinal",
// Under `"organizeImportsCollation": "unicode"`,
// what is the current locale? Can be:
// - [any other locale code]
// - "auto" (use the editor's locale)
"organizeImportsLocale": "en",
// Under `"organizeImportsCollation": "unicode"`,
// should upper-case letters or lower-case letters come first? Can be:
// - false (locale-specific)
// - "upper"
// - "lower"
"organizeImportsCaseFirst": false,
// Under `"organizeImportsCollation": "unicode"`,
// do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
// - true
// - false
"organizeImportsNumericCollation": true,
// Under `"organizeImportsCollation": "unicode"`,
// do letters with accent marks/diacritics get sorted distinctly
// from their "base" letter (i.e. is é different from e)? Can be
// - true
// - false
"organizeImportsAccentCollation": true
},
"javascript.unstable": {
// same options valid here...
},
}

你可以阅读有关 自动检测和指定不区分大小写的原始工作 的更多详细信息,然后阅读 更广泛的选项集

¥You can read more details on the original work for auto-detecting and specifying case-insensitivity, followed by the the broader set of options.

详尽的 switch/case 补全

¥Exhaustive switch/case Completions

编写 switch 语句时,TypeScript 现在会检测被检查的值是否具有字面类型。如果是这样,它将提供一个补全,为每个未覆盖的 case 提供支持。

¥When writing a switch statement, TypeScript now detects when the value being checked has a literal type. If so, it will offer a completion that scaffolds out each uncovered case.

A set of case statements generated through auto-completion based on literal types.

你可以 在 GitHub 上查看实现的细节

¥You can see specifics of the implementation on GitHub.

速度、内存和包大小优化

¥Speed, Memory, and Package Size Optimizations

TypeScript 5.0 对我们的代码结构、数据结构和算法实现进行了许多重大改进。所有这些意味着你的整体体验应该更快。 - 不仅仅是运行 TypeScript,甚至安装它。

¥TypeScript 5.0 contains lots of powerful changes across our code structure, our data structures, and algorithmic implementations. What these all mean is that your entire experience should be faster - not just running TypeScript, but even installing it.

以下是一些我们在速度和大小方面相对于 TypeScript 4.9 的有趣改进。

¥Here are a few interesting wins in speed and size that we’ve been able to capture relative to TypeScript 4.9.

场景 相对于 TS 4.9 的时间或大小
material-ui 构建时间 89%
TypeScript 编译器启动时间 89%
Playwright 构建时间 88%
TypeScript 编译器自构建时间 87%
Outlook Web 构建时间 82%
VS Code 构建时间 80%
TypeScript npm 软件包大小 59%

Chart of build/run times and package size of TypeScript 5.0 relative to TypeScript 4.9: material-ui docs build time: 89%; Playwright build time: 88%; tsc startup time: 87%; tsc build time: 87%; Outlook Web build time: 82%; VS Code build time: 80%; typescript Package Size: 59%

如何实现?有一些值得注意的改进,我们希望在未来提供更多细节。但是,我们不会让你等待那篇博文。

¥How? There are a few notable improvements we’d like give more details on in the future. But we won’t make you wait for that blog post.

首先,我们最近将 TypeScript 从命名空间迁移到了模块,这使我们能够利用可以执行作用域提升等优化的现代构建工具。使用此工具,重新审视我们的打包策略,并删除一些弃用的代码,已将 TypeScript 4.9 的 63.8 MB 包大小减少了约 26.4 MB。它还通过直接函数调用显著提升了速度。

¥First off, we recently migrated TypeScript from namespaces to modules, allowing us to leverage modern build tooling that can perform optimizations like scope hoisting. Using this tooling, revisiting our packaging strategy, and removing some deprecated code has shaved off about 26.4 MB from TypeScript 4.9’s 63.8 MB package size. It also brought us a notable speed-up through direct function calls.

TypeScript 还为编译器中的内部对象类型增加了更多一致性,并且精简了存储在某些对象类型上的数据。这减少了多态和超态的使用点,同时抵消了统一形状所需的大部分内存消耗。

¥TypeScript also added more uniformity to internal object types within the compiler, and also slimmed the data stored on some of these object types as well. This reduced polymorphic and megamorphic use sites, while offsetting most of the necessary memory consumption that was necessary for uniform shapes.

我们还在将信息序列化为字符串时执行了一些缓存。类型显示可以作为错误报告、声明触发、代码补全等的一部分进行,最终可能会非常昂贵。TypeScript 现在会缓存一些常用机制,以便在这些操作之间重用。

¥We’ve also performed some caching when serializing information to strings. Type display, which can happen as part of error reporting, declaration emit, code completions, and more, can end up being fairly expensive. TypeScript now caches some commonly used machinery to reuse across these operations.

我们做出的另一个显著改进是利用 var 偶尔避开在闭包中使用 letconst 的成本,从而改进了我们的解析器。这提升了我们的部分解析性能。

¥Another notable change we made that improved our parser was leveraging var to occasionally side-step the cost of using let and const across closures. This improved some of our parsing performance.

总的来说,我们预计大多数代码库的速度应该会从 TypeScript 5.0 开始有所提升,并且能够持续复现 10% 到 20% 之间的提升。当然,这取决于硬件和代码库的特性,但我们鼓励你立即在你的代码库上尝试一下!

¥Overall, we expect most codebases should see speed improvements from TypeScript 5.0, and have consistently been able to reproduce wins between 10% to 20%. Of course this will depend on hardware and codebase characteristics, but we encourage you to try it out on your codebase today!

更多信息,请参阅我们的一些显著优化:

¥For more information, see some of our notable optimizations:

重大变更和弃用

¥Breaking Changes and Deprecations

运行时要求

¥Runtime Requirements

TypeScript 现在支持 ECMAScript 2018。对于 Node 用户,这意味着最低版本要求至少为 Node.js 10 及更高版本。

¥TypeScript now targets ECMAScript 2018. For Node users, that means a minimum version requirement of at least Node.js 10 and later.

lib.d.ts 变更

¥lib.d.ts Changes

更改 DOM 类型的生成方式可能会对现有代码产生影响。值得注意的是,某些属性已从 number 转换为数字字面量类型,并且用于剪切、复制和粘贴事件处理的属性和方法已跨接口移动。

¥Changes to how types for the DOM are generated might have an impact on existing code. Notably, certain properties have been converted from number to numeric literal types, and properties and methods for cut, copy, and paste event handling have been moved across interfaces.

API 重大变更

¥API Breaking Changes

在 TypeScript 5.0 中,我们迁移到了模块,移除了一些不必要的接口,并进行了一些正确性改进。更多变更详情,请访问我们的 API 重大变更 页面。

¥In TypeScript 5.0, we moved to modules, removed some unnecessary interfaces, and made some correctness improvements. For more details on what’s changed, see our API Breaking Changes page.

关系运算符中禁止隐式强制转换

¥Forbidden Implicit Coercions in Relational Operators

如果你编写的代码可能导致隐式字符串到数字的强制转换,TypeScript 中的某些操作会触发警告:

¥Certain operations in TypeScript will already warn you if you write code which may cause an implicit string-to-number coercion:

ts
function func(ns: number | string) {
return ns * 4; // Error, possible implicit coercion
}

在 5.0 中,这也将应用于关系运算符 ><<=>=

¥In 5.0, this will also be applied to the relational operators >, <, <=, and >=:

ts
function func(ns: number | string) {
return ns > 4; // Now also an error
}

如果需要,可以使用 + 显式将操作数强制转换为 number

¥To allow this if desired, you can explicitly coerce the operand to a number using +:

ts
function func(ns: number | string) {
return +ns > 4; // OK
}

正确性改进Mateusz Burzyński 贡献。

¥This correctness improvement was contributed courtesy of Mateusz Burzyński.

枚举全面改进

¥Enum Overhaul

自首次发布以来,TypeScript 在 enum 方面一直存在一些怪异之处。在 5.0 中,我们正在清理其中一些问题,并减少理解可声明的各种 enum 类型所需的概念数量。

¥TypeScript has had some long-standing oddities around enums ever since its first release. In 5.0, we’re cleaning up some of these problems, as well as reducing the concept count needed to understand the various kinds of enums you can declare.

你可能会看到两个主要的新错误。首先,将域外的字面量赋值给 enum 类型现在会像预期的那样出错:

¥There are two main new errors you might see as part of this. The first is that assigning an out-of-domain literal to an enum type will now error as one might expect:

ts
enum SomeEvenDigit {
Zero = 0,
Two = 2,
Four = 4
}
// Now correctly an error
let m: SomeEvenDigit = 1;

另一个问题是,某些类型的间接混合字符串/数字 enum 形式的声明会错误地创建一个全数字的 enum

¥The other is that declaration of certain kinds of indirected mixed string/number enum forms would, incorrectly, create an all-number enum:

ts
enum Letters {
A = "a"
}
enum Numbers {
one = 1,
two = Letters.A
}
// Now correctly an error
const t: number = Numbers.two;

你可以 查看相关变更中的更多详细信息

¥You can see more details in relevant change.

--experimentalDecorators 下,构造函数中参数装饰器的类型检查更精确

¥More Accurate Type-Checking for Parameter Decorators in Constructors Under --experimentalDecorators

TypeScript 5.0 使 --experimentalDecorators 下的装饰器类型检查更加准确。在构造函数参数上使用装饰器时,这一点会变得显而易见。

¥TypeScript 5.0 makes type-checking more accurate for decorators under --experimentalDecorators. One place where this becomes apparent is when using a decorator on a constructor parameter.

ts
export declare const inject:
(entity: any) =>
(target: object, key: string | symbol, index?: number) => void;
export class Foo {}
export class C {
constructor(@inject(Foo) private x: any) {
}
}

此调用将失败,因为 key 期望的是 string | symbol,但构造函数参数接收的键是 undefined。正确的修复方法是在 inject 中更改 key 的类型。如果你使用的库无法升级,一个合理的解决方法是将 inject 封装在类型更安全的装饰器函数中,并对 key 使用类型断言。

¥This call will fail because key expects a string | symbol, but constructor parameters receive a key of undefined. The correct fix is to change the type of key within inject. A reasonable workaround if you’re using a library that can’t be upgraded is is to wrap inject in a more type-safe decorator function, and use a type-assertion on key.

详情请见 查看此问题

¥For more details, see this issue.

弃用和默认变更

¥Deprecations and Default Changes

在 TypeScript 5.0 中,我们弃用了以下设置和设置值:

¥In TypeScript 5.0, we’ve deprecated the following settings and setting values:

  • --target: ES3

  • --out

  • --noImplicitUseStrict

  • --keyofStringsOnly

  • --suppressExcessPropertyErrors

  • --suppressImplicitAnyIndexErrors

  • --noStrictGenericChecks

  • --charset

  • --importsNotUsedAsValues

  • --preserveValueImports

  • 项目引用中的 prepend

    ¥prepend in project references

这些配置将持续有效,直到 TypeScript 5.5 正式版发布,届时它们将被完全移除。但是,如果你使用这些设置,将会收到警告。在 TypeScript 5.0 以及未来的 5.1、5.2、5.3 和 5.4 版本中,你可以指定 "ignoreDeprecations": "5.0" 来静音这些警告。我们很快将发布一个 4.9 补丁,允许指定 ignoreDeprecations,以便更平滑地升级。除了弃用部分功能外,我们还更改了一些设置,以更好地改善 TypeScript 的跨平台行为。

¥These configurations will continue to be allowed until TypeScript 5.5, at which point they will be removed entirely, however, you will receive a warning if you are using these settings. In TypeScript 5.0, as well as future releases 5.1, 5.2, 5.3, and 5.4, you can specify "ignoreDeprecations": "5.0" to silence those warnings. We’ll also shortly be releasing a 4.9 patch to allow specifying ignoreDeprecations to allow for smoother upgrades. Aside from deprecations, we’ve changed some settings to better improve cross-platform behavior in TypeScript.

--newLine,控制 JavaScript 文件中触发的行尾,过去如果未指定,则根据当前操作系统推断。我们认为构建应该尽可能具有确定性,而且 Windows 记事本现在支持换行符,因此新的默认设置是 LF。旧版特定于操作系统的推断行为已不再可用。

¥--newLine, which controls the line endings emitted in JavaScript files, used to be inferred based on the current operating system if not specified. We think builds should be as deterministic as possible, and Windows Notepad supports line-feed line endings now, so the new default setting is LF. The old OS-specific inference behavior is no longer available.

--forceConsistentCasingInFileNames,确保项目中所有对同一文件名的引用都使用大小写一致,现在默认为 true。这可以帮助捕获在不区分大小写的文件系统上编写的代码的差异问题。

¥--forceConsistentCasingInFileNames, which ensured that all references to the same file name in a project agreed in casing, now defaults to true. This can help catch differences issues with code written on case-insensitive file systems.

你可以留下反馈并查看有关 跟踪 5.0 版本弃用问题 的更多信息。

¥You can leave feedback and view more information on the tracking issue for 5.0 deprecations