TypeScript 4.3
属性上独立的写入类型
在 JavaScript 中,API 在存储之前转换传入的值是很常见的。 这通常也发生在 getter 和 setter 上。 例如,假设我们有一个类,其 setter 在将值保存到私有字段之前总是将其转换为 number。
class Thing {
#size = 0;
get size() {
return this.#size;
}
set size(value) {
let num = Number(value);
// 不允许 NaN 等。
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}
this.#size = num;
}
}Try我们如何在 TypeScript 中为这段 JavaScript 代码编写类型? 嗯,从技术上讲,我们在这里不需要做任何特殊的事情——TypeScript 可以在没有显式类型的情况下查看它,并可以推断出 size 是一个数字。
问题在于 size 允许你赋值不仅仅是 number 类型。 我们可以通过说 size 具有 unknown 或 any 类型来解决这个问题,就像在这个代码片段中一样:
class Thing {
// ...
get size(): unknown {
return this.#size;
}
}但那不好——unknown 强制读取 size 的人进行类型断言,而 any 不会捕获任何错误。 如果我们真的想对转换值的 API 进行建模,以前版本的 TypeScript 迫使我们在精确性(使读取值更容易,写入更难)和宽容性(使写入值更容易,读取更难)之间做出选择。
这就是为什么 TypeScript 4.3 允许你为属性的读取和写入指定类型。
class Thing {
#size = 0;
get size(): number {
return this.#size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// 不允许 NaN 等。
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}
this.#size = num;
}
}Try在上面的例子中,我们的 set 访问器接受更广泛的类型(string、boolean 和 number),但我们的 get 访问器始终保证它是 number。 现在我们终于可以将其他类型赋值给这些属性而不会出错!
let thing = new Thing();
// 将其他类型赋值给 `thing.size` 有效!
thing.size = "hello";
thing.size = true;
thing.size = 42;
// 读取 `thing.size` 总是产生一个数字!
let mySize: number = thing.size;Try当考虑两个同名的属性如何相互关联时,TypeScript 将只使用“读取”类型(例如上面 get 访问器的类型)。 “写入”类型仅在直接写入属性时被考虑。
请记住,这不仅仅局限于类的模式。 你可以在对象字面量中编写具有不同类型 getter 和 setter 的代码。
function makeThing(): Thing {
let size = 0;
return {
get size(): number {
return size;
},
set size(value: string | number | boolean) {
let num = Number(value);
// 不允许 NaN 等。
if (!Number.isFinite(num)) {
size = 0;
return;
}
size = num;
},
};
}实际上,我们已经在接口/对象类型中添加了语法来支持属性上不同的读/写类型。
// 现在有效!
interface Thing {
get size(): number
set size(value: number | string | boolean);
}对属性使用不同的读取和写入类型的一个限制是,读取属性的类型必须可赋值给正在写入的类型。 换句话说,getter 类型必须可赋值给 setter。 这确保了某种程度的一致性,使得一个属性始终可以赋值给自己。
有关此功能的更多信息,请查看实现拉取请求。
override 和 --noImplicitOverride 标志
在 JavaScript 中扩展类时,语言使得覆盖方法变得非常容易(双关语)——但不幸的是,你可能会遇到一些错误。
一个很大的问题是遗漏重命名。 例如,考虑以下类:
class SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}SpecializedComponent 是 SomeComponent 的子类,并覆盖了 show 和 hide 方法。 如果有人决定删除 show 和 hide,并用一个单一方法替换它们,会发生什么?
class SomeComponent {
- show() {
- // ...
- }
- hide() {
- // ...
- }
+ setVisible(value: boolean) {
+ // ...
+ }
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}哦不! 我们的 SpecializedComponent 没有更新。 现在它只是添加了两个可能永远不会被调用的无用方法 show 和 hide。
部分问题在于用户无法明确他们是打算添加新方法还是覆盖现有方法。 这就是为什么 TypeScript 4.3 添加了 override 关键字。
class SpecializedComponent extends SomeComponent {
override show() {
// ...
}
override hide() {
// ...
}
}当一个方法用 override 标记时,TypeScript 将始终确保基类中存在同名的方法。
class SomeComponent {
setVisible(value: boolean) {
// ...
}
}
class SpecializedComponent extends SomeComponent {
override show() {
}
}Try这是一个很大的改进,但如果你忘记在方法上写 override,它就没有帮助——这也是用户可能遇到的一个大错误。
例如,你可能不小心“覆盖”了基类中存在的方法而没有意识到。
class Base {
someHelperMethod() {
// ...
}
}
class Derived extends Base {
// 哎呀!我们并不是要在这里覆盖,
// 我们只需要编写一个本地辅助方法。
someHelperMethod() {
// ...
}
}这就是为什么 TypeScript 4.3 也提供了一个新的 noImplicitOverride 标志。 当启用此选项时,除非你显式使用 override 关键字,否则覆盖超类中的任何方法都会成为错误。 在最后一个例子中,TypeScript 会在 noImplicitOverride 下报错,并给我们一个提示,我们可能需要在 Derived 中重命名我们的方法。
我们要感谢我们的社区对此的实现。 这些项目的工作是由 Wenlu Wang 在一个拉取请求中实现的,不过早期由 Paul Cody Johnston 实现的仅包含 override 关键字的拉取请求为方向和讨论奠定了基础。 我们对他们为此功能投入的时间表示感谢。
模板字符串类型改进
在最近的版本中,TypeScript 引入了一种新的类型构造:模板字符串类型。 这些类型通过连接构造新的类字符串类型...
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// 等同于
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";...或者匹配其他类字符串类型的模式。
declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
// 有效!
s1 = s2;我们做的第一个更改是关于 TypeScript 何时推断模板字符串类型。 当一个模板字符串被一个类似字符串字面量的类型上下文类型化时(即当 TypeScript 看到我们将一个模板字符串传递给某个接受字面量类型的东西时),它将尝试给该表达式一个模板类型。
function bar(s: string): `hello ${string}` {
// 以前是错误,现在有效!
return `hello ${s}`;
}这也适用于推断类型,并且类型参数 extends string
declare let s: string;
declare function f<T extends string>(x: T): T;
// 以前:string
// 现在:`hello ${string}`
let x2 = f(`hello ${s}`);第二个主要变化是 TypeScript 现在可以更好地关联和推断不同的模板字符串类型。
要看到这一点,请考虑以下示例代码:
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 将很好地确定它们是否真正兼容。
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}`;
// 现在 *所有这些* 都有效!
s1 = s2;
s1 = s3;
s1 = s4;
s1 = s5;
s1 = s6;在做这项工作的同时,我们也确保添加了更好的推断能力。 你可以在实践中看到这些例子:
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}*`
}有关更多信息,请查看关于利用上下文类型的原始拉取请求,以及改进模板类型之间推断和检查的拉取请求。
ECMAScript #private 类成员
TypeScript 4.3 扩展了类中可以赋予 #private #names 的成员,使它们在运行时真正私有。 除了属性之外,方法和访问器也可以赋予私有名称。
class Foo {
#someMethod() {
//...
}
get #someValue() {
return 100;
}
publicMethod() {
// 这些有效。
// 我们可以在类内部访问私有命名的成员。
this.#someMethod();
return this.#someValue;
}
}
new Foo().#someMethod();
// ~~~~~~~~~~~
// 错误!
// 属性 '#someMethod' 在类 'Foo' 外部不可访问,因为它有一个私有标识符。
new Foo().#someValue;
// ~~~~~~~~~~
// 错误!
// 属性 '#someValue' 在类 'Foo' 外部不可访问,因为它有一个私有标识符。更广泛地说,静态成员现在也可以具有私有名称。
class Foo {
static #someMethod() {
// ...
}
}
Foo.#someMethod();
// ~~~~~~~~~~~
// 错误!
// 属性 '#someMethod' 在类 'Foo' 外部不可访问,因为它有一个私有标识符。此功能由我们彭博社的朋友在一个拉取请求中编写——由 Titian Cernicova-Dragomir 和 Kubilay Kahveci 编写,并得到 Joey Watts、Rob Palmer 和 Tim McClure 的支持和专业知识。 我们向他们所有人表示感谢!
ConstructorParameters 适用于抽象类
在 TypeScript 4.3 中,ConstructorParameters 类型辅助函数现在适用于 abstract 类。
abstract class C {
constructor(a: string, b: number) {
// ...
}
}
// 具有类型 '[a: string, b: number]'。
type CParams = ConstructorParameters<typeof C>;这要归功于 TypeScript 4.2 中所做的工作,其中构造签名可以被标记为抽象:
type MyConstructorOf<T> = {
abstract new(...args: any[]): T;
}
// 或使用简写语法:
type MyConstructorOf<T> = abstract new (...args: any[]) => T;你可以在 GitHub 上更详细地查看此更改。
泛型的上下文收窄
TypeScript 4.3 现在在泛型值上包含了一些更智能的类型收窄逻辑。 这使得 TypeScript 可以接受更多的模式,有时甚至可以捕获错误。
作为一些动机,假设我们试图编写一个名为 makeUnique 的函数。 它将接受一个 Set 或一个元素的 Array,如果是 Array,它将对该 Array 进行排序,并根据某个比较函数删除重复项。 之后,它将返回原始集合。
function makeUnique<T>(
collection: Set<T> | T[],
comparer: (x: T, y: T) => number
): Set<T> | T[] {
// 如果是 Set,提前退出。
// 我们假设元素已经是唯一的。
if (collection instanceof Set) {
return collection;
}
// 对数组排序,然后删除连续的重复项。
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[] 的位置来实现这一点。
- 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 及更早版本中,一旦你尝试这样做,你就会遇到一堆错误。
function makeUnique<T, C extends Set<T> | T[]>(
collection: C,
comparer: (x: T, y: T) => number
): C {
// 如果是 Set,提前退出。
// 我们假设元素已经是唯一的。
if (collection instanceof Set) {
return collection;
}
// 对数组排序,然后删除连续的重复项。
collection.sort(comparer);
// ~~~~
// 错误:类型 'C' 上不存在属性 'sort'。
for (let i = 0; i < collection.length; i++) {
// ~~~~~~
// 错误:类型 'C' 上不存在属性 'length'。
let j = i;
while (
j < collection.length &&
comparer(collection[i], collection[j + 1]) === 0
) {
// ~~~~~~
// 错误:类型 'C' 上不存在属性 'length'。
// ~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
// 错误:由于类型 'number' 的表达式不能用于索引类型 'Set<T> | T[]',因此元素隐式具有 'any' 类型。
j++;
}
collection.splice(i + 1, j - i);
// ~~~~~~
// 错误:类型 'C' 上不存在属性 'splice'。
}
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 会拒绝。
function makeUnique<T>(
collection: Set<T> | T[],
comparer: (x: T, y: T) => number
): Set<T> | T[] {
// 如果是 Set,提前退出。
// 我们假设元素已经是唯一的。
if (collection instanceof Set) {
return collection;
// ~~~~~~~~~~
// 错误:类型 'Set<T>' 不能赋值给类型 'C'。
// 'Set<T>' 可赋值给类型 'C' 的约束,但
// 'C' 可能用约束 'Set<T> | T[]' 的不同子类型实例化。
}
// ...
return collection;
// ~~~~~~~~~~
// 错误:类型 'T[]' 不能赋值给类型 'C'。
// 'T[]' 可赋值给类型 'C' 的约束,但
// 'C' 可能用约束 'Set<T> | T[]' 的不同子类型实例化。
}那么 TypeScript 4.3 是如何改变的呢? 嗯,基本上在编写代码的几个关键地方,类型系统真正关心的只是类型的约束。 例如,当我们编写 collection.length 时,TypeScript 并不关心 collection 具有类型 C 的事实,它只关心可用的属性,这些属性由约束 T[] | Set<T> 确定。
在这种情况下,TypeScript 将获取约束的收窄类型,因为这将给你你关心的数据; 然而,在任何其他情况下,我们只会尝试收窄原始泛型类型(并且通常会得到原始的泛型类型)。
换句话说,根据你使用泛型值的方式,TypeScript 会以略有不同的方式收窄它。 最终结果是上面的整个示例在编译时没有类型检查错误。
有关更多详细信息,你可以查看 GitHub 上的原始拉取请求。
总是真值 Promise 检查
在 strictNullChecks 下,在条件中检查 Promise 是否为“真值”将触发错误。
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) {
// ~~~~~
// 错误!
// 此条件将始终返回 true,因为此 'Promise<boolean>' 似乎总是已定义。
// 你忘记使用 'await' 了吗?
return "true";
}
return "false";
}此更改由 Jack Works 贡献,我们对此表示感谢!
static 索引签名
索引签名允许我们在值上设置比类型显式声明的更多的属性。
class Foo {
hello = "hello";
world = 1234;
// 这是一个索引签名:
[propName: string]: string | number | undefined;
}
let instance = new Foo();
// 有效赋值
instance["whatever"] = 42;
// 具有类型 'string | number | undefined'。
let x = instance["something"];到目前为止,索引签名只能在类的实例侧声明。 由于 Wenlu Wang 的一个拉取请求,索引签名现在可以声明为 static。
class Foo {
static hello = "hello";
static world = 1234;
static [propName: string]: string | number | undefined;
}
// 有效。
Foo["whatever"] = 42;
// 具有类型 'string | number | undefined'
let x = Foo["something"];同样的规则适用于类的静态侧的索引签名,就像适用于实例侧一样——也就是说,每个其他静态属性必须与索引签名兼容。
class Foo {
static prop = true;
// ~~~~
// 错误!类型为 'boolean' 的属性 'prop'
// 不能赋值给字符串索引类型
// 'string | number | undefined'。
static [propName: string]: string | number | undefined;
}.tsbuildinfo 大小改进
在 TypeScript 4.3 中,作为 incremental 构建的一部分生成的 .tsbuildinfo 文件应该会显著变小。 这要归功于内部格式的几项优化,创建了具有数字标识符的表,在整个文件中使用,而不是重复完整的路径和类似信息。 这项工作由 Tobias Koppers 在他们的拉取请求中牵头,为后续的拉取请求和进一步的优化提供了灵感。
我们已经看到 .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 帮助我们找到这些改进机会!
导入语句补全
用户在 JavaScript 中遇到的最大痛点之一是导入和导出语句的顺序——具体来说,导入是写成
import { func } from "./module.js";而不是
from "./module.js" import { func };这在从头开始编写完整的导入语句时会带来一些痛苦,因为自动补全无法正常工作。 例如,如果你开始编写类似 import { 的内容,TypeScript 不知道你打算从哪个模块导入,因此无法提供任何缩小范围的补全。
为了解决这个问题,我们利用了自动导入的功能! 自动导入已经处理了无法从特定模块缩小补全范围的问题——它们的全部意义在于提供每个可能的导出,并自动在文件顶部插入导入语句。
因此,当你现在开始编写一个没有路径的 import 语句时,我们将为你提供可能的导入列表。 当你提交一个补全时,我们将完成完整的导入语句,包括你将要编写的路径。

这项工作需要编辑器专门支持该功能。 你可以使用最新的 Visual Studio Code Insiders 版本来尝试一下。
有关更多信息,请查看实现拉取请求!
对 @link 标签的编辑器支持
TypeScript 现在可以理解 @link 标签,并将尝试解析它们链接到的声明。 这意味着你将能够在 @link 标签内的名称上悬停并获取快速信息,或使用跳转到定义或查找所有引用等命令。
例如,在下面的示例中,你将能够跳转到 @link plantCarrot 中的 plantCarrot 的定义,并且支持 TypeScript 的编辑器将跳转到 plantCarrot 的函数声明。
/**
* 在 {@link plantCarrot} 之后 70 到 80 天调用。
*/
function harvestCarrot(carrot: Carrot) {}
/**
* 为了获得最佳效果,在早春调用。在 v2.1.0 中添加。
* @param seed 确保它是胡萝卜种子!
*/
function plantCarrot(seed: Seed) {
// TODO: 一些园艺工作
}
有关更多信息,请查看 GitHub 上的拉取请求!
在非 JavaScript 文件路径上跳转到定义
许多加载器允许用户使用 JavaScript 导入将资产包含在他们的应用程序中。 它们通常被写成类似于 import "./styles.css" 之类的东西。
到目前为止,TypeScript 的编辑器功能甚至不会尝试读取此文件,因此跳转到定义通常会失败。 在最坏的情况下,跳转到定义会跳转到类似 declare module "*.css" 的声明(如果它能找到类似的东西)。
TypeScript 的语言服务现在在相对文件路径上执行跳转到定义时,会尝试跳转到正确的文件,即使它们不是 JavaScript 或 TypeScript 文件! 尝试导入 CSS、SVG、PNG、字体文件、Vue 文件等。
有关更多信息,你可以查看实现拉取请求。
破坏性更改
lib.d.ts 更改
与每个 TypeScript 版本一样,lib.d.ts 的声明(尤其是为 Web 上下文生成的声明)已经更改。 在此版本中,我们利用 Mozilla 的 browser-compat-data 删除了没有浏览器实现的 API。 虽然你不太可能使用它们,但诸如 Account、AssertionOptions、RTCStatsEventInit、MSGestureEvent、DeviceLightEvent、MSPointerEvent、ServiceWorkerMessageEvent 和 WebAuthentication 等 API 都已从 lib.d.ts 中删除。 这在这里有更详细的讨论。
https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/991
useDefineForClassFields 现在在 esnext 上默认为 true,最终在 es2022 上也是
2021 年,类字段特性被添加到 JavaScript 规范中,其行为与 TypeScript 实现它的方式不同。为了准备这一变化,在 TypeScript 3.7 中,添加了一个标志(useDefineForClassFields)来迁移到输出的 JavaScript,以匹配 JavaScript 标准行为。
现在该特性已存在于 JavaScript 中,我们将为 ES2022 及以上版本(包括 ESNext)将默认值更改为 true。
总是真值 Promise 检查的错误
在 strictNullChecks 下,在条件检查中使用一个似乎总是已定义的 Promise 现在被视为错误。
declare var p: Promise<number>;
if (p) {
// ~
// 错误!
// 此条件将始终返回 true,因为此 'Promise<number>' 似乎总是已定义。
//
// 你忘记使用 'await' 了吗?
}有关更多详细信息,请参阅原始更改。
联合枚举不能与任意数字比较
某些 enum 在其成员自动填充或简单编写时被视为联合 enum。 在这些情况下,枚举可以回忆起它可能代表的每个值。
在 TypeScript 4.3 中,如果具有联合 enum 类型的值与一个它永远不可能相等的数字字面量进行比较,那么类型检查器将发出错误。
enum E {
A = 0,
B = 1,
}
function doSomething(x: E) {
// 错误!此条件将始终返回 'false',因为类型 'E' 和 '-1' 没有重叠。
if (x === -1) {
// ...
}
}作为一种解决方法,你可以重写注解以包含适当的字面量类型。
enum E {
A = 0,
B = 1,
}
// 如果我们真的确定 -1 可能出现,则将 -1 包含在类型中。
function doSomething(x: E | -1) {
if (x === -1) {
// ...
}
}你也可以在值上使用类型断言。
enum E {
A = 0,
B = 1,
}
function doSomething(x: E) {
// 对 'x' 使用类型断言,因为我们知道我们实际上不仅仅处理来自 'E' 的值。
if ((x as number) === -1) {
// ...
}
}或者,你可以重新声明你的枚举,使其具有非平凡的初始化器,以便任何数字都可以赋值给该枚举并与之比较。如果意图是让枚举指定几个众所周知的值,这可能很有用。
enum E {
// 0 前面的 + 使 TypeScript 不再推断为联合枚举。
A = +0,
B = 1,
}有关更多详细信息,请参阅原始更改