TypeScript 4.3

属性的独立写入类型

在 JavaScript 中,API 在存储传入的值之前对其进行转换是非常常见的做法。这在 getter 和 setter 中也经常发生。例如,设想我们有一个类,其中的 setter 在将值存入私有字段之前总是将其转换为 number

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 是一个数字。

问题在于 size 允许你为其赋不仅仅是 number 类型的值。我们可以通过将 size 的类型声明为 unknownany 来绕过这个问题,如下面的代码片段所示。

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

但这样做并不好——unknown 会强制读取 size 的人进行类型断言,而 any 则无法捕获任何错误。如果我们确实想要建模这种转换值的 API,以往版本的 TypeScript 强迫我们在“精确”(使读取值更容易,但写入更难)和“宽泛”(使写入值更容易,但读取更难)之间做出选择。

这就是 TypeScript 4.3 允许你分别为属性的读取和写入指定类型的原因。

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。现在,我们终于可以向这些属性赋其他类型的值而不会报错了!

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 访问器上的类型)。“写入”类型仅在直接写入属性时才会被考虑。

请记住,这种模式并不局限于类。你也可以在对象字面量中编写具有不同类型的 getter 和 setter。

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;
},
};
}

事实上,我们已经在接口/对象类型中添加了语法,以支持属性上不同的读取/写入类型。

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

使用不同的类型读取和写入属性的一个限制是,读取属性的类型必须可分配给写入属性的类型。换句话说,getter 的类型必须可分配给 setter。这确保了一定程度的一致性,使得属性始终可以赋值给自己。

关于此功能的更多信息,请查看 实现此功能的 PR

override--noImplicitOverride 标志

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

一个主要的错误是错过了重命名。例如,看看下面的类。

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 方法,它们可能永远不会被调用。

这里部分问题在于用户无法明确他们是打算添加一个新方法,还是打算重写一个现有方法。这就是为什么 TypeScript 4.3 添加了 override 关键字。

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

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

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,它就没用了——这也是用户可能犯的一个大错。

例如,你可能会在不经意间“践踏”了基类中存在的方法而不自知。

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 内部重命名我们的方法。

我们要感谢社区在这里所做的实现。这些功能的实现工作是由 Wenlu Wang 通过 一个 PR 完成的,尽管由 Paul Cody Johnston 实现仅 override 关键字的早期 PR 为方向和讨论提供了基础。我们对他们投入时间开发这些功能表示感谢。

模板字符串类型改进

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

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 看到我们将一个模板字符串传给接受字面量类型的东西时),它会尝试给该表达式一个模板类型。

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

这也适用于类型推断以及 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 现在可以更好地关联不同模板字符串类型之间的关系,并在它们之间进行推断

要看到这一点,请看以下示例代码。

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 这样的操作根本无法工作。

TypeScript 现在确实在做这项工作来证明模板字符串的每个部分是否能成功匹配。你现在可以混合使用具有不同替换的模板字符串,TypeScript 将能很好地弄清楚它们是否真的兼容。

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;

在做这项工作时,我们也确保添加了更好的推断能力。你可以看到这些功能的实际操作示例。

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}*`
}

更多信息,请参阅 利用上下文类型的原始 PR,以及 改进模板类型之间推断和检查的 PR

ECMAScript #private 类元素

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

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.

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

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

此功能是由我们在 Bloomberg 的朋友们编写的 PR 实现的,由 Titian Cernicova-DragomirKubilay Kahveci 编写,并得到了 Joey WattsRob PalmerTim McClure 的支持和专业建议。我们要向他们所有人表示感谢!

ConstructorParameters 适用于抽象类

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

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

这归功于 TypeScript 4.2 中的工作,在该版本中,构造签名可以被标记为抽象。

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

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

泛型的上下文收窄

TypeScript 4.3 现在对泛型值包含了一些更智能的类型收窄逻辑。这使得 TypeScript 能够接受更多模式,有时甚至能捕捉到错误。

作为动机,假设我们正在尝试编写一个名为 makeUnique 的函数。它接收一个元素的 SetArray,如果给定一个 Array,它将对该 Array 进行排序并根据某些比较函数移除重复项。最后,它将返回原始集合。

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 及更早版本中,一旦你尝试这样做,就会遇到一堆错误。

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 对我们这么刻薄?

问题在于,当我们执行 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> 决定。

在这种情况下,TypeScript 会获取约束的收窄类型,因为它会给你你关心的数据;然而,在任何其他情况下,我们只会尝试收窄原始泛型类型(通常最终得到原始泛型类型)。

换句话说,基于你如何使用泛型值,TypeScript 会以不同的方式收窄它。最终结果是,整个上面的示例在没有类型检查错误的情况下编译通过。

更多详细信息,你可以 查看 GitHub 上的原始 PR

始终为真的 Promise 检查

strictNullChecks 下,在条件判断中检查 Promise 是否为“真值”将触发错误。

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 索引签名

索引签名允许我们为值设置比类型显式声明更多的属性。

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 WangPR,索引签名现在可以声明为 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"];

类静态端的索引签名适用与实例端相同的规则——即每个其他静态属性都必须与索引签名兼容。

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 大小改进

在 TypeScript 4.3 中,作为 incremental 构建的一部分生成的 .tsbuildinfo 文件应该会显著变小。这归功于内部格式的几项优化,创建了带有数字标识符的表格以便在整个文件中使用,而不是重复完整路径和类似信息。这项工作由 Tobias Koppers他的 PR 中带头完成,并启发了 随后的 PR进一步的优化

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

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

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

--incremental--watch 编译中更懒惰的计算

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

这就是为什么除了 .tsbuildinfo 大小改进之外,TypeScript 4.3 还对 incremental--watch 模式进行了一些更改,使得具有这些标志的项目的第一次构建与普通构建一样快!为了做到这一点,通常提前计算的许多信息被改为在后续构建中按需计算。虽然这可能会给后续构建增加一些开销,但 TypeScript 的 incremental--watch 功能通常仍将在更小的文件集上运行,并且任何需要的信息随后都会被保存。从某种意义上说,incremental--watch 构建会“预热”,并在你更新几次文件后加快编译速度。

在一个拥有 3000 个文件的存储库中,这使初始构建时间缩短到了近三分之一

这项工作由 Tobias Koppers 开始,其工作促成了 此功能的最终更改。我们非常感谢 Tobias 帮助我们找到这些改进机会!

Import 语句补全

用户在使用 JavaScript 中的 import 和 export 语句时遇到的最大痛点之一是顺序——特别是 imports 的书写方式为:

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

而不是:

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

当从零开始写一个完整的 import 语句时,这会造成一些麻烦,因为自动补全无法正常工作。例如,如果你开始写 import { 之类的东西,TypeScript 不知道你打算从哪个模块导入,因此它无法提供任何缩小范围的补全。

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

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

Import statement completions

这项工作需要专门支持该功能的编辑器。你可以通过使用最新的 Visual Studio Code Insiders 版本 来尝试这个功能。

更多信息,请查看 实现此功能的 PR

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

例如,在下面的示例中,你可以在 @link plantCarrot 中的 plantCarrot 上跳转到定义,TypeScript 支持的编辑器将跳转到 plantCarrot 的函数声明。

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 上的 PR

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

许多加载器允许用户使用 JavaScript import 在应用程序中包含资产。它们通常写成 import "./styles.css" 之类的形式。

到目前为止,TypeScript 的编辑器功能甚至不会尝试读取此文件,因此跳转到定义通常会失败。充其量,跳转到定义会跳转到类似 declare module "*.css" 的声明(如果它能找到类似的东西)。

TypeScript 的语言服务现在会在你对相对文件路径执行跳转到定义时尝试跳转到正确的文件,即使它们不是 JavaScript 或 TypeScript 文件!尝试使用 CSS、SVG、PNG、字体文件、Vue 文件等的 import 来体验它。

更多信息,你可以查看 实现此功能的 PR

破坏性变更

lib.d.ts 变更

与每个 TypeScript 版本一样,lib.d.ts 的声明(特别是为 Web 上下文生成的声明)发生了变化。在此版本中,我们利用了 Mozilla 的浏览器兼容性数据 来删除没有任何浏览器实现的 API。虽然你不太可能正在使用它们,但 AccountAssertionOptionsRTCStatsEventInitMSGestureEventDeviceLightEventMSPointerEventServiceWorkerMessageEventWebAuthentication 等 API 已从 lib.d.ts 中删除。这在 此处 进行了详细讨论。

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

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

2021 年,类字段功能被添加到 JavaScript 规范中,其行为与 TypeScript 的实现方式不同。为准备此更改,在 TypeScript 3.7 中添加了一个标志(useDefineForClassFields),以将编译出的 JavaScript 迁移到符合 JavaScript 标准行为。

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

始终为真的 Promise 检查报错

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

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'?
}

更多详细信息,请参阅原始更改

联合枚举不能与任意数字进行比较

某些 enum 在其成员被自动填充或简单书写时被视为联合 enum。在这种情况下,枚举可以回溯它可能表示的每个值。

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

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) {
// ...
}
}

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

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) {
// ...
}
}

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

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) {
// ...
}
}

或者,你可以重新声明你的枚举以具有非琐碎的初始化程序,以便任何数字对该枚举都是可分配和可比较的。如果意图是枚举指定几个众所周知的值,这可能会很有用。

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

更多详细信息,请参阅原始更改

TypeScript 文档是一个开源项目。通过 提交 Pull Request 帮助我们改进这些页面 ❤

此页面的贡献者
OTOrta Therox (3)
ELEliran Levi (1)
NTNémeth Tamás (1)
MSMax Strübing (1)
JBJack Bates (1)

最后更新:2026 年 3 月 27 日