在最后一次赋值后保留闭包中的收缩
🌐 Preserved Narrowing in Closures Following Last Assignments
TypeScript 通常可以根据你可能进行的检查为变量推断出更具体的类型。这个过程称为类型收窄。
🌐 TypeScript can usually figure out a more specific type for a variable based on checks that you might perform. This process is called narrowing.
tsfunction uppercaseStrings(x: string | number) {if (typeof x === "string") {// TypeScript knows 'x' is a 'string' here.return x.toUpperCase();}}
一个常见的痛点是,这些缩小的类型并不总是在函数闭包中保留。
🌐 One common pain point was that these narrowed types weren’t always preserved within function closures.
tsfunction getUrls(url: string | URL, names: string[]) {if (typeof url === "string") {url = new URL(url);}return names.map(name => {url.searchParams.set("name", name)// ~~~~~~~~~~~~// error!// Property 'searchParams' does not exist on type 'string | URL'.return url.toString();});}
在这里,TypeScript 决定在我们的回调函数中假设 url 实际上是 URL 对象是不“安全”的,因为它在其他地方被修改了;然而,在这种情况下,该箭头函数总是在赋值给 url 之后创建的,而且它也是赋值给 url 的最后一次赋值。
🌐 Here, TypeScript decided that it wasn’t “safe” to assume that url was actually a URL object in our callback function because it was mutated elsewhere;
however, in this instance, that arrow function is always created after that assignment to url, and it’s also the last assignment to url.
TypeScript 5.4 利用这一点让类型缩小变得更智能。当参数和 let 变量在非提升 函数中使用时,类型检查器会寻找最后的赋值点。如果找到了,TypeScript 就可以安全地从包含函数之外进行类型缩小。这意味着上述示例现在可以正常工作。
🌐 TypeScript 5.4 takes advantage of this to make narrowing a little smarter.
When parameters and let variables are used in non-hoisted functions, the type-checker will look for a last assignment point.
If one is found, TypeScript can safely narrow from outside the containing function.
What that means is the above example just works now.
请注意,如果变量在嵌套函数中的任何地方被赋值,则收窄分析不会生效。这是因为无法确定该函数是否会在以后被调用。
🌐 Note that narrowing analysis doesn’t kick in if the variable is assigned anywhere in a nested function. This is because there’s no way to know for sure whether the function will be called later.
tsfunction printValueLater(value: string | undefined) {if (value === undefined) {value = "missing!";}setTimeout(() => {// Modifying 'value', even in a way that shouldn't affect// its type, will invalidate type refinements in closures.value = value;}, 500);setTimeout(() => {console.log(value.toUpperCase());// ~~~~~// error! 'value' is possibly 'undefined'.}, 1000);}
这应该让许多典型的 JavaScript 代码更容易表达。你可以在 GitHub 上阅读有关此更改的更多信息。
🌐 This should make lots of typical JavaScript code easier to express. You can read more about the change on GitHub.
NoInfer 工具类型
🌐 The NoInfer Utility Type
调用泛型函数时,TypeScript 能够根据传入的内容推断出类型参数。
🌐 When calling generic functions, TypeScript is able to infer type arguments from whatever you pass in.
tsfunction doSomething<T>(arg: T) {// ...}// We can explicitly say that 'T' should be 'string'.doSomething<string>("hello!");// We can also just let the type of 'T' get inferred.doSomething("hello!");
然而,一个挑战是,并不总是清楚应该推断哪种“最佳”类型。这可能导致 TypeScript 拒绝有效的调用、接受有问题的调用,或者在捕捉到错误时仅报告更差的错误信息。
🌐 One challenge, however, is that it is not always clear what the “best” type is to infer. This might lead to TypeScript rejecting valid calls, accepting questionable calls, or just reporting worse error messages when it catches a bug.
例如,我们可以想象一个 createStreetLight 函数,它接受一个颜色名称列表,以及一个可选的默认颜色。
🌐 For example, let’s imagine a createStreetLight function that takes a list of color names, along with an optional default color.
tsfunction createStreetLight<C extends string>(colors: C[], defaultColor?: C) {// ...}createStreetLight(["red", "yellow", "green"], "red");
当我们传入一个原本不在 colors 数组中的 defaultColor 会发生什么?
在这个函数中,colors 应该是“真实来源”,并描述可以传递给 defaultColor 的内容。
🌐 What happens when we pass in a defaultColor that wasn’t in the original colors array?
In this function, colors is supposed to be the “source of truth” and describe what can be passed to defaultColor.
ts// Oops! This is undesirable, but is allowed!createStreetLight(["red", "yellow", "green"], "blue");
在这个调用中,类型推断确定 "blue" 与 "red"、"yellow" 或 "green" 一样有效。因此,TypeScript 并没有拒绝这个调用,而是将 C 的类型推断为 "red" | "yellow" | "green" | "blue"。你可能会说,推断结果简直让我们大吃一惊!
🌐 In this call, type inference decided that "blue" was just as valid of a type as "red" or "yellow" or "green".
So instead of rejecting the call, TypeScript infers the type of C as "red" | "yellow" | "green" | "blue".
You might say that inference just blue up in our faces!
目前人们处理此问题的一种方法是添加一个单独的类型参数,该参数受现有类型参数的限制。
🌐 One way people currently deal with this is to add a separate type parameter that’s bounded by the existing type parameter.
tsfunction createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) {}createStreetLight(["red", "yellow", "green"], "blue");// ~~~~~~// error!// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.
这可以工作,但有点尴尬,因为 D 可能在 createStreetLight 的签名中其他地方都不会使用。
虽然在这种情况下并不算坏,但在签名中只使用一次类型参数通常是一种代码味道(不好的代码习惯)。
🌐 This works, but is a little bit awkward because D probably won’t be used anywhere else in the signature for createStreetLight.
While not bad in this case, using a type parameter only once in a signature is often a code smell.
这就是为什么 TypeScript 5.4 引入了一个新的 NoInfer<T> 工具类型。将一个类型用 NoInfer<...> 封装起来,向 TypeScript 发出信号,不去深入匹配内部类型以寻找类型推断的候选项。
🌐 That’s why TypeScript 5.4 introduces a new NoInfer<T> utility type.
Surrounding a type in NoInfer<...> gives a signal to TypeScript not to dig in and match against the inner types to find candidates for type inference.
使用 NoInfer,我们可以将 createStreetLight 重写成如下形式:
🌐 Using NoInfer, we can rewrite createStreetLight as something like this:
tsfunction createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) {// ...}createStreetLight(["red", "yellow", "green"], "blue");// ~~~~~~// error!// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.
排除 defaultColor 类型被用于推断意味着 "blue" 永远不会成为推断候选类型,类型检查器可以因此拒绝它。
🌐 Excluding the type of defaultColor from being explored for inference means that "blue" never ends up as an inference candidate, and the type-checker can reject it.
你可以在实现的拉取请求中看到具体的更改,以及感谢Mateusz Burzyński提供的初始实现!
🌐 You can see the specific changes in the implementing pull request, along with the initial implementation provided thanks to Mateusz Burzyński!
Object.groupBy 和 Map.groupBy
🌐 Object.groupBy and Map.groupBy
TypeScript 5.4 为 JavaScript 的新 Object.groupBy 和 Map.groupBy 静态方法添加了声明。
🌐 TypeScript 5.4 adds declarations for JavaScript’s new Object.groupBy and Map.groupBy static methods.
Object.groupBy 接受一个可迭代对象,以及一个决定每个元素应放入哪个“组”的函数。该函数需要为每个不同的组生成一个“键”,而 Object.groupBy 使用该键创建一个对象,其中每个键对应的值是一个包含原始元素的数组。
因此,以下 JavaScript 代码:
🌐 So the following JavaScript:
jsconst array = [0, 1, 2, 3, 4, 5];const myObj = Object.groupBy(array, (num, index) => {return num % 2 === 0 ? "even": "odd";});
基本上等同于这样写:
🌐 is basically equivalent to writing this:
jsconst myObj = {even: [0, 2, 4],odd: [1, 3, 5],};
Map.groupBy 类似,但它生成 Map 而不是普通对象。如果你需要 Map 的保证,或者正在处理期望 Map 的 API,或者需要使用任何类型的键进行分组——不仅仅是可以作为 JavaScript 属性名的键——这种方式可能更可取。
jsconst myObj = Map.groupBy(array, (num, index) => {return num % 2 === 0 ? "even" : "odd";});
就像之前一样,你也可以用相同的方式创建 myObj:
🌐 and just as before, you could have created myObj in an equivalent way:
jsconst myObj = new Map();myObj.set("even", [0, 2, 4]);myObj.set("odd", [1, 3, 5]);
请注意,在上面 Object.groupBy 的示例中,生成的对象使用了所有可选属性。
🌐 Note that in the above example of Object.groupBy, the object produced uses all optional properties.
tsinterface EvenOdds {even?: number[];odd?: number[];}const myObj: EvenOdds = Object.groupBy(...);myObj.even;// ~~~~// Error to access this under 'strictNullChecks'.
这是因为无法以通用方式保证所有密钥都是由 groupBy 生成的。
🌐 This is because there’s no way to guarantee in a general way that all the keys were produced by groupBy.
请注意,这些方法只有在将你的 target 配置为 esnext 或调整你的 lib 设置时才能访问。我们预计它们最终会在稳定的 es2024 目标下可用。
🌐 Note also that these methods are only accessible by configuring your target to esnext or adjusting your lib settings.
We expect they will eventually be available under a stable es2024 target.
我们想向 Kevin Gibbons 表示感谢,感谢他 为这些 groupBy 方法添加声明。
🌐 We’d like to extend a thanks to Kevin Gibbons for adding the declarations to these groupBy methods.
在 --moduleResolution bundler 和 --module preserve 中支持 require() 调用
🌐 Support for require() calls in --moduleResolution bundler and --module preserve
TypeScript 有一个名为 bundler 的 moduleResolution 选项,用于模拟现代打包工具确定导入路径对应文件的方式。该选项的一个限制是必须与 --module esnext 配合使用,因此无法使用 import ... = require(...) 语法。
🌐 TypeScript has a moduleResolution option called bundler that is meant to model the way modern bundlers figure out which file an import path refers to.
One of the limitations of the option is that it had to be paired with --module esnext, making it impossible to use the import ... = require(...) syntax.
ts// previously erroredimport myModule = require("module/path");
如果你只是打算编写标准 ECMAScript import,这可能看起来没什么大不了的,但在使用带有条件导出的包时就有所不同。
🌐 That might not seem like a big deal if you’re planning on just writing standard ECMAScript imports, but there’s a difference when using a package with conditional exports.
在 TypeScript 5.4 中,当将 module 设置为一个名为 preserve 的新选项时,现在可以使用 require()。
🌐 In TypeScript 5.4, require() can now be used when setting the module setting to a new option called preserve.
在 --module preserve 和 --moduleResolution bundler 之间,这两者更准确地模拟了像 Bun 这样的打包工具和运行时允许的行为,以及它们如何执行模块查找。
事实上,在使用 --module preserve 时,bundler 选项将会自动为 --moduleResolution(以及 --esModuleInterop 和 --resolveJsonModule)设置。
🌐 Between --module preserve and --moduleResolution bundler, the two more accurately model what bundlers and runtimes like Bun will allow, and how they’ll perform module lookups.
In fact, when using --module preserve, the bundler option will be implicitly set for --moduleResolution (along with --esModuleInterop and --resolveJsonModule)
json{"compilerOptions": {"module": "preserve",// ^ also implies:// "moduleResolution": "bundler",// "esModuleInterop": true,// "resolveJsonModule": true,// ...}}
在 --module preserve 下,ECMAScript import 将始终按原样输出,而 import ... = require(...) 将作为 require() 调用输出(尽管实际上你可能甚至不会使用 TypeScript 来进行输出,因为你很可能会为你的代码使用打包工具)。
这对于包含文件的文件扩展名无论如何都适用。
所以这段代码的输出是:
🌐 Under --module preserve, an ECMAScript import will always be emitted as-is, and import ... = require(...) will be emitted as a require() call (though in practice you may not even use TypeScript for emit, since it’s likely you’ll be using a bundler for your code).
This holds true regardless of the file extension of the containing file.
So the output of this code:
tsimport * as foo from "some-package/foo";import bar = require("some-package/bar");
应该看起来像这样:
🌐 should look something like this:
jsimport * as foo from "some-package/foo";var bar = require("some-package/bar");
这也意味着,你选择的语法决定了条件导出是如何匹配的。因此,在上面的例子中,如果 some-package 的 package.json 看起来像这样:
🌐 What this also means is that the syntax you choose directs how conditional exports are matched.
So in the above example, if the package.json of some-package looks like this:
json{"name": "some-package","version": "0.0.1","exports": {"./foo": {"import": "./esm/foo-from-import.mjs","require": "./cjs/foo-from-require.cjs"},"./bar": {"import": "./esm/bar-from-import.mjs","require": "./cjs/bar-from-require.cjs"}}}
TypeScript 会将这些路径解析为 [...]/some-package/esm/foo-from-import.mjs 和 [...]/some-package/cjs/bar-from-require.cjs。
🌐 TypeScript will resolve these paths to [...]/some-package/esm/foo-from-import.mjs and [...]/some-package/cjs/bar-from-require.cjs.
欲了解更多信息,你可以在此查看这些新设置。
🌐 For more information, you can read up on these new settings here.
已检查的导入属性和断言
🌐 Checked Import Attributes and Assertions
现在会根据全局 ImportAttributes 类型检查导入属性和断言。这意味着运行时现在可以更准确地描述导入属性
🌐 Import attributes and assertions are now checked against the global ImportAttributes type.
This means that runtimes can now more accurately describe the import attributes
ts// In some global file.interface ImportAttributes {type: "json";}// In some other moduleimport * as ns from "foo" with { type: "not-json" };// ~~~~~~~~~~// error!//// Type '{ type: "not-json"; }' is not assignable to type 'ImportAttributes'.// Types of property 'type' are incompatible.// Type '"not-json"' is not assignable to type '"json"'.
这个更改得益于 Oleksandr Tarasiuk 的提供:链接。
快速修复添加缺失参数的问题
🌐 Quick Fix for Adding Missing Parameters
TypeScript 现在提供了一个快速修复方法,可以为使用过多参数调用的函数添加新形参。
🌐 TypeScript now has a quick fix to add a new parameter to functions that are called with too many arguments.


当将新参数传递到多个现有函数时,这很有用,而这在当今可能很麻烦。
🌐 This can be useful when threading a new argument through several existing functions, which can be cumbersome today.
这个快速修复 是由 Oleksandr Tarasiuk 提供的。
TypeScript 5.0 弃用功能即将带来的变更
🌐 Upcoming Changes from TypeScript 5.0 Deprecations
TypeScript 5.0 弃用了以下选项和行为:
🌐 TypeScript 5.0 deprecated the following options and behaviors:
charsettarget: ES3importsNotUsedAsValuesnoImplicitUseStrictnoStrictGenericCheckskeyofStringsOnlysuppressExcessPropertyErrorssuppressImplicitAnyIndexErrorsoutpreserveValueImports- 项目引用中的
prepend - 隐式操作系统特定的
newLine
为了继续使用它们,使用 TypeScript 5.0 及更高版本的开发者必须指定一个名为 ignoreDeprecations 的新选项,并将其值设置为 "5.0"。
然而,TypeScript 5.4 将是这些功能仍然正常工作的最后一个版本。到 TypeScript 5.5(可能在 2024 年 6 月),这些将变成严重错误,使用它们的代码需要迁移。
欲了解更多信息,你可以在 GitHub 上阅读此计划,其中包含有关如何最佳地调整你的代码库的建议。
值得注意的行为变更
🌐 Notable Behavioral Changes
本节重点介绍了一些值得注意的变化,这些变化在任何升级中都应予以认可和理解。有时,它会高亮已弃用、移除和新的限制。它还可能包含作为功能性改进的错误修复,但这些修复也可能通过引入新的错误影响现有构建。
🌐 This section highlights a set of noteworthy changes that should be acknowledged and understood as part of any upgrade. Sometimes it will highlight deprecations, removals, and new restrictions. It can also contain bug fixes that are functionally improvements, but which can also affect an existing build by introducing new errors.
lib.d.ts 变更
🌐 lib.d.ts Changes
为 DOM 生成的类型可能会影响对代码库的类型检查。更多信息,请参见 TypeScript 5.4 的 DOM 更新。
🌐 Types generated for the DOM may have an impact on type-checking your codebase. For more information, see the DOM updates for TypeScript 5.4.
更精确的条件类型约束
🌐 More Accurate Conditional Type Constraints
以下代码不再允许在函数 foo 中进行第二次变量声明。
🌐 The following code no longer allows the second variable declaration in the function foo.
tstype IsArray<T> = T extends any[] ? true : false;function foo<U extends object>(x: IsArray<U>) {let first: true = x; // Errorlet second: false = x; // Error, but previously wasn't}
以前,当 TypeScript 检查 second 的初始化值时,它需要确定 IsArray<U> 是否可以赋值给单位类型 false。
虽然 IsArray<U> 没有明显的兼容方式,TypeScript 也会查看该类型的约束。
在像 T extends Foo ? TrueBranch : FalseBranch 这样的条件类型中,如果 T 是泛型,类型系统会查看 T 的约束,将其替换到 T 本身,并决定选择 true 分支还是 false 分支。
🌐 Previously, when TypeScript checked the initializer for second, it needed to determine whether IsArray<U> was assignable to the unit type false.
While IsArray<U> isn’t compatible any obvious way, TypeScript looks at the constraint of that type as well.
In a conditional type like T extends Foo ? TrueBranch : FalseBranch, where T is generic, the type system would look at the constraint of T, substitute it in for T itself, and decide on either the true or false branch.
但这种行为是不准确的,因为它过于急切。即使 T 的约束不能赋值给 Foo,也不意味着它不会被实例化为可以赋值的类型。因此,更正确的做法是在无法证明 T 从不 或 总是 扩展 Foo. 的情况下,为条件类型的约束生成一个联合类型。
🌐 But this behavior was inaccurate because it was overly eager.
Even if the constraint of T isn’t assignable to Foo, that doesn’t mean that it won’t be instantiated with something that is.
And so the more correct behavior is to produce a union type for the constraint of the conditional type in cases where it can’t be proven that T never or always extends Foo.
TypeScript 5.4 采用了这种更精确的行为。 实际上的意思是,你可能会发现一些条件类型的实例不再与它们的分支兼容。
🌐 TypeScript 5.4 adopts this more accurate behavior. What this means in practice is that you may begin to find that some conditional type instances are no longer compatible with their branches.
更积极地减少类型变量和原始类型之间的交叉
🌐 More Aggressive Reduction of Intersections Between Type Variables and Primitive Types
TypeScript 现在会更积极地减少与类型变量和原语的交叉,具体取决于类型变量的约束与这些原语的重叠程度。
🌐 TypeScript now reduces intersections with type variables and primitives more aggressively, depending on how the type variable’s constraint overlaps with those primitives.
tsdeclare function intersect<T, U>(x: T, y: U): T & U;function foo<T extends "abc" | "def">(x: T, str: string, num: number) {// Was 'T & string', now is just 'T'let a = intersect(x, str);// Was 'T & number', now is just 'never'let b = intersect(x, num)// Was '(T & "abc") | (T & "def")', now is just 'T'let c = Math.random() < 0.5 ?intersect(x, "abc") :intersect(x, "def");}
欲了解更多信息,请点击此处查看更改。
🌐 For more information, see the change here.
改进了使用插值对模板字符串的检查
🌐 Improved Checking Against Template Strings with Interpolations
TypeScript 现在可以更准确地检查字符串是否可以分配给模板字符串类型的占位符槽。
🌐 TypeScript now more accurately checks whether or not strings are assignable to the placeholder slots of a template string type.
tsfunction a<T extends {id: string}>() {let x: `-${keyof T & string}`;// Used to error, now doesn't.x = "-id";}
此行为是更理想的,但可能会导致使用条件类型等结构的代码中断,因为这些规则的更改很容易被察觉。
🌐 This behavior is more desirable, but may cause breaks in code using constructs like conditional types, where these rule changes are easy to witness.
查看此更改了解详情。
仅类型导入与本地值冲突时的错误
🌐 Errors When Type-Only Imports Conflict with Local Values
以前,如果对 Something 的导入仅引用了类型,TypeScript 会允许 isolatedModules 下的以下代码。
🌐 Previously, TypeScript would permit the following code under isolatedModules if the import to Something only referred to a type.
tsimport { Something } from "./some/path";let Something = 123;
然而,即使代码在运行时必然会失败,单文件编译器也不能擅自认为删除 import 是“安全”的。在 TypeScript 5.4 中,这段代码会触发如下错误:
🌐 However, it’s not safe for single-file compilers to assume whether it’s “safe” to drop the import, even if the code is guaranteed to fail at runtime.
In TypeScript 5.4, this code will trigger an error like the following:
Import 'Something' conflicts with local value, so must be declared with a type-only import when 'isolatedModules' is enabled.
解决方法应该是要么进行本地重命名,要么如错误提示所示,在导入中添加 type 修饰符:
🌐 The fix should be to either make a local rename, or, as the error states, add the type modifier to the import:
tsimport type { Something } from "./some/path";// orimport { type Something } from "./some/path";
新的枚举可赋值性限制
🌐 New Enum Assignability Restrictions
当两个枚举具有相同的声明名称和枚举成员名称时,它们以前总是被认为是兼容的;然而,当数值已知时,TypeScript 会默默地允许它们具有不同的值。
🌐 When two enums have the same declared names and enum member names, they were previously always considered compatible; however, when the values were known, TypeScript would silently allow them to have differing values.
TypeScript 5.4 通过要求已知值时必须相同来加强此限制。
🌐 TypeScript 5.4 tightens this restriction by requiring the values to be identical when they are known.
tsnamespace First {export enum SomeEnum {A = 0,B = 1,}}namespace Second {export enum SomeEnum {A = 0,B = 2,}}function foo(x: First.SomeEnum, y: Second.SomeEnum) {// Both used to be compatible - no longer the case,// TypeScript errors with something like://// Each declaration of 'SomeEnum.B' differs in its value, where '1' was expected but '2' was given.x = y;y = x;}
此外,对于枚举成员没有静态已知值的情况,引入了新的限制。在这些情况下,另一个枚举至少必须是隐式数值型(例如,它没有静态解析的初始化器),或者它是显式数值型(意味着 TypeScript 可以将其值解析为某个数值)。实际上,这意味着字符串枚举成员仅与具有相同值的其他字符串枚举兼容。
🌐 Additionally, there are new restrictions for when one of the enum members does not have a statically known value. In these cases, the other enum must at least be implicitly numeric (e.g. it has no statically resolved initializer), or it is explicitly numeric (meaning TypeScript could resolve the value to something numeric). Practically speaking, what this means is that string enum members are only ever compatible with other string enums of the same value.
tsnamespace First {export declare enum SomeEnum {A,B,}}namespace Second {export declare enum SomeEnum {A,B = "some known string",}}function foo(x: First.SomeEnum, y: Second.SomeEnum) {// Both used to be compatible - no longer the case,// TypeScript errors with something like://// One value of 'SomeEnum.B' is the string '"some known string"', and the other is assumed to be an unknown numeric value.x = y;y = x;}
欲了解更多信息,请参阅引入此更改的拉取请求 see the pull request that introduced this change。
🌐 For more information, see the pull request that introduced this change.
枚举成员的名称限制
🌐 Name Restrictions on Enum Members
TypeScript 不再允许枚举成员使用名称 Infinity、-Infinity 或 NaN。
🌐 TypeScript no longer allows enum members to use the names Infinity, -Infinity, or NaN.
ts// Errors on all of these://// An enum member cannot have a numeric name.enum E {Infinity = 0,"-Infinity" = 1,NaN = 2,}
使用 any 剩余元素更好地在元组上保留映射类型
🌐 Better Mapped Type Preservation Over Tuples with any Rest Elements
以前,将 any 映射类型应用到元组中会创建一个 any 元素类型。这是不可取的,现在已经修复。
🌐 Previously, applying a mapped type with any into a tuple would create an any element type.
This is undesirable and is now fixed.
tsPromise.all(["", ...([] as any)]).then((result) => {const head = result[0]; // 5.3: any, 5.4: stringconst tail = result.slice(1); // 5.3 any, 5.4: any[]});
欲了解更多信息,请参阅修复,以及关于行为变化的后续讨论和进一步的调整。
🌐 For more information, see the fix along with the follow-on discussion around behavioral changes and further tweaks.
Emit 变更
🌐 Emit Changes
虽然这本身不是破坏性更改,但开发者可能已经隐式地依赖于 TypeScript 的 JavaScript 或声明输出。以下是值得注意的更改。
🌐 While not a breaking change per se, developers may have implicitly taken dependencies on TypeScript’s JavaScript or declaration emit outputs. The following are notable changes.