最后一次赋值后,闭包中保留了收缩的范围
TypeScript
通常可以根据您执行的检查推断出变量的更具体类型。这个过程称为收缩。
1 | function uppercaseStrings(x: string | number) { |
一个常见的问题是这些收缩的类型并不总是在函数闭包中保留。
1 | function getUrls(url: string | URL, names: string[]) { |
在这里,TypeScript
决定在我们的回调函数中不“安全”假设url
实际上是一个URL
对象,因为它在其他地方被改变;然而,在这种情况下,箭头函数总是在对url
进行分配之后创建的,并且它也是对url的最后一次赋值。
TypeScript 5.4
利用这一点,使收缩范围更加智能。当参数和let
变量用于非提升函数时,类型检查器将寻找最后一次赋值点。如果找到一个,TypeScript
可以安全地从包含函数的外部收缩范围。这意味着上面的示例现在可以正常工作了。
请注意,如果变量在嵌套函数中的任何地方赋值,收缩分析就不会起作用。这是因为无法确定该函数是否稍后会被调用。
1 | function printValueLater(value: string | undefined) { |
这应该使得许多典型的JavaScript
代码更容易表达。您可以在GitHub
上阅读更多关于这个改变的内容。
NoInfer 实用类型
在调用泛型函数时,TypeScript
能够从您传递的任何内容中推断类型参数。
1 | function doSomething<T>(arg: T) { |
然而,一个挑战是,并不总是清楚什么是最好的类型来推断。这可能导致 TypeScript
拒绝有效的调用,接受可疑的调用,或者在捕获到错误时提供更糟糕的错误消息。
例如,让我们想象一个 createStreetLight
函数,它接受一个颜色名称列表,以及一个可选的默认颜色。
1 | function createStreetLight<C extends string>(colors: C[], defaultColor?: C) { |
当我们传入一个原始颜色数组中不存在的 defaultColor
时会发生什么?在这个函数中,colors
应该是“真实的来源”并描述可以传递给defaultColor
的内容。
1 | // 糟糕!这是不希望的,但是允许的! |
在这个调用中,类型推断决定"blue"
与"red"
、"yellow"
或"green"
一样有效。所以 TypeScript
不会拒绝该调用,而是推断类型 C 为"red" | "yellow" | "green" | "blue"
。你可能会说,这个推论让我们颜面无光!
人们目前处理这个问题的一种方法是添加一个由现有类型参数限定的单独类型参数。
1 | function createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) { |
这样做是可行的,但有点尴尬,因为 D
可能在 createStreetLight
签名中的其他地方不会被使用。虽然在这种情况下不算糟糕,但在签名中只使用一次类型参数通常是一个代码异味。
这就是为什么 TypeScript 5.4
引入了一个新的 NoInfer<T>
实用类型。将类型包裹在 NoInfer<...>
中向 TypeScript
发出一个信号,告诉它不要深入匹配内部类型以寻找推断候选项。
使用 NoInfer
,我们可以将 createStreetLight
重写为类似这样的东西:
1 | function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { |
将 defaultColor
的类型排除在推断之外意味着"blue"
永远不会成为推断候选项,类型检查器可以拒绝它。
您可以在实现的拉取请求中看到具体的更改,以及由 Mateusz Burzyński
提供的初始实现!
Object.groupBy 和 Map.groupBy
TypeScript 5.4
添加了 JavaScript
的新Object.groupBy
和Map.groupBy
静态方法的声明。
Object.groupBy
接受一个可迭代对象和一个函数,该函数决定每个元素应该放入哪个“组”。该函数需要为每个不同的组制作一个“键”,而 Object.groupBy
则使用该键来创建一个对象,其中每个键映射到包含原始元素的数组。
因此,以下 JavaScript
代码:
1 | const array = [0, 1, 2, 3, 4, 5]; |
基本上等同于编写:
1 | const myObj = { |
Map.groupBy
类似,但生成一个 Map
而不是普通对象。如果您需要Map
的保证、处理预期为 Map
的 API
,或者需要使用任何类型的键进行分组——而不仅仅是 JavaScript
中可用作属性名称的键,那么这可能更可取。
1 | const myObj = Map.groupBy(array, (num, index) => { |
就像之前一样,您可以以等效的方式创建 myObj
:
1 | const myObj = new Map(); |
请注意,在上述 Object.groupBy
示例中,生成的对象使用了所有可选属性。
1 | interface EvenOdds { |
这是因为在一般情况下无法保证所有键都由 groupBy
生成。
还请注意,只有通过配置目标为 esnext
或调整lib
设置才能访问这些方法。我们预计它们最终将在稳定的 es2024
目标下可用。
我们要感谢 Kevin Gibbons
添加了这些 groupBy
方法的声明。
--moduleResolution bundler
和 --module preserve
对 require()
调用的支持
TypeScript
有一个名为 bundler
的 moduleResolution
选项,旨在模拟现代打包工具确定导入路径所指向的文件的方式。该选项的一个限制是它必须与 --module esnext
配对使用,这使得无法使用 import ... = require(...)
语法。
1 | // 以前会报错 |
如果您打算只编写标准的ECMAScript
导入,这可能看起来并不重要,但是在使用具有条件导出的包时就会有所不同。
在 TypeScript 5.4
中,当将 module
设置为一个名为 preserve
的新选项时,现在可以使用 require()
。
在 --module preserve
和 --moduleResolution bundler
之间,两者更准确地模拟了像 Bun
这样的打包工具和运行时会允许什么,并且它们将执行模块查找的方式。事实上,当使用 --module preserve
时,bundler
选项将隐式设置为 --moduleResolution
(以及 --esModuleInterop
和 --resolveJsonModule
)。
1 | { |
在 --module preserve
下,ECMAScript
导入将始终按原样输出,并且 import ... = require(...)
将被输出为require()
调用(尽管实际上您可能根本不会使用 TypeScript
进行输出,因为很可能您将使用打包工具对您的代码进行打包)。无论包含文件的扩展名是什么,这一点都成立。因此,此代码的输出:
1 | import * as foo from "some-package/foo"; |
应该看起来像这样:
1 | import * as foo from "some-package/foo"; |
这也意味着您选择的语法将指导条件导出的匹配方式。因此,在上面的示例中,如果 some-package
的 package.json
如下所示:
1 | { |
TypeScript
将这些路径解析为 [...]/some-package/esm/foo-from-import.mjs
和 [...]/some-package/cjs/bar-from-require.cjs
。
已检查的导入属性和断言
现在,导入属性和断言会根据全局 ImportAttributes
类型进行检查。这意味着运行时现在可以更准确地描述导入属性。
1 | // 在某个全局文件中。 |
这个变化是由 Oleksandr Tarasiuk
提供的。
添加缺少参数的快速修复
TypeScript
现在有了一个快速修复,可以向调用参数过多的函数中添加新参数。
当 someFunction
调用 someHelperFunction
比预期参数多 2 个时,会提供一个快速修复。
在应用快速修复之后,缺少的参数已经添加到 someHelperFunction
中。
当通过多个现有函数传递新参数时,这可能会很有用,这在当前情况下可能会很繁琐。
这个快速修复是由 Oleksandr Tarasiuk
提供的。
TypeScript 5.0 即将弃用
TypeScript 5.0
弃用了以下选项和行为:
- target: ES3
- noImplicitUseStrict
- keyofStringsOnly
- suppressExcessPropertyErrors
- suppressImplicitAnyIndexErrors
- noStrictGenericChecks
- charset
- out
- 在项目引用中的 prepend
- 隐式的 OS 特定的 newLine
为了继续使用它们,使用 TypeScript 5.0
和其他更新版本的开发人员必须指定一个名为 ignoreDeprecations
的新选项,其值为 "5.0"
。
然而,TypScript 5.4
将是这些选项继续正常工作的最后一个版本。到 TypeScript 5.5
(可能是 2024 年 6 月),这些选项将变为硬错误,使用它们的代码将需要迁移。
破坏性更改
lib.d.ts 更改
为 DOM
生成的类型可能会影响您的代码库。有关 TypeScript 5.4
的 DOM
更新的更多信息,请参阅此处。
更准确的条件类型约束
在函数 foo
中,以下代码不再允许第二个变量声明。
1 | type IsArray<T> = T extends any[] ? true : false; |
以前,当 TypeScrip
t 检查 second
的初始化程序时,它需要确定 IsArray<U>
是否可分配给单元类型 false
。虽然 IsArray<U>
在明显的方式下不兼容,但 TypeScript
也会查看该类型的约束。在条件类型 T extends Foo ? TrueBranch : FalseBranch
中,T
是泛型,类型系统会查看 T
的约束,将其替换为 T
本身,并决定是 true
还是false
分支。
但这种行为是不准确的,因为它太急切了。即使 T
的约束不能分配给 Foo
,这并不意味着它不会被实例化为可分配给 Foo
的东西。因此,更正确的行为是在无法证明 T 从不或总是扩展为 Foo
的情况下,为条件类型的约束产生一个联合类型。
TypeScript 5.4
采用了这种更准确的行为。实际上,这意味着您可能开始发现一些条件类型实例不再与它们的分支兼容。
更积极地减少类型变量与原始类型之间的交集
TypeScript
现在更积极地减少类型变量与原始类型之间的交集,具体取决于类型变量的约束与这些原始类型的重叠程度。
1 | declare function intersect<T, U>(x: T, y: U): T & U; |
有关更多信息,请参阅此处的更改。
TypeScript 现在更准确地检查字符串是否可分配给模板字符串类型的占位符位置。
1 | function a<T extends {id: string}>() { |
这种行为更可取,但可能会导致使用诸如条件类型之类的构造的代码中断。
有关更多详细信息,请参阅此更改。
当类型仅导入与本地值冲突时出现错误
以前,如果 Something
的导入仅引用了一个类型,在 isolatedModules
下,TypeScript
将允许以下代码。
1 | import { Something } from "./some/path"; |
但是,单文件编译器无法安全地假设是否“安全”删除导入,即使代码在运行时肯定会失败。在 TypeScript 5.4
中,这段代码将触发如下错误:
1 | Import 'Something' conflicts with local value, so must be declared with a type-only import when 'isolatedModules' is enabled. |
修复应该是要么进行本地重命名,要么按照错误中所述,向导入添加类型修饰符:
1 | import type { Something } from "./some/path"; |
有关更改本身的更多信息,请参见此处。
提交更改
虽然不是一个破坏性的变化,开发人员可能已经隐式地依赖于 TypeScript
的 JavaScript
或声明发射输出。以下是值得注意的更改。
- 更经常地保留类型参数名称当被阴影化时
- 将异步函数的复杂参数列表移动到下级生成器主体中
- 不要删除函数声明中的绑定别名
- 在
ImportTypeNode
中的ImportAttributes
应该经历相同的提交阶段
接下来是什么?
到目前为止,TypeScript 5.4
是我们所说的“功能稳定”。TypeScript 5.4
的重点将是bug
修复、优化和某些低风险的编辑器功能。我们将在一个多月内提供一个发行候选版本,随后很快发布一个稳定版本。如果您有兴趣规划发布,请务必关注我们的迭代计划,其中包含目标发布日期等信息。
请注意:虽然 beta 是尝试 TypeScript
下一个版本的好方法,但您也可以尝试夜间版本来获取最新的 TypeScript 5.4
版本,直到我们发布候选版本为止。我们的夜间版本经过了充分测试,甚至可以仅在您的编辑器中进行测试。