TypeScript 5.8 Beta 新特性解析
原文链接:https://devblogs.microsoft.com/typescript/announcing-typescript-5-8-beta/
作者:Daniel Rosenwasser
译者:倔强青铜三
前言
大家好,我是倔强青铜三。作为一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新。欢迎关注我的微信公众号:倔强青铜三。如果你觉得这篇文章对你有帮助,别忘了点赞、收藏和关注哦,一键三连支持我吧!
TypeScript 5.8 Beta版本已经发布,带来了许多令人兴奋的新特性和改进。本文将详细介绍这些新特性,帮助你更好地了解和使用TypeScript 5.8。
条件类型和索引访问类型的检查返回值
假设有一个API,允许用户选择多个选项:
/**
* @param prompt 提示文本
* @param selectionKind 用户可以选择单个或多个选项
* @param items 提供给用户的选择项
**/
async function showQuickPick(
prompt: string,
selectionKind: SelectionKind,
items: readonly string[],
): Promise<string | string[]> {
// ...
}
enum SelectionKind {
Single,
Multiple,
}
showQuickPick
函数的意图是根据selectionKind
参数决定返回单个字符串还是字符串数组。然而,当前的类型签名并没有明确这一点,它只是返回string | string[]
。这导致调用者需要手动检查返回值类型。
例如,以下代码中,我们期望shoppingList
是一个字符串数组,但实际上它的类型是string | string[]
,导致调用join
方法时报错:
let shoppingList = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"],
);
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
// error!
// Property 'join' does not exist on type 'string | string[]'.
为了更精确地定义返回类型,我们可以使用条件类型:
type QuickPickReturn<S extends SelectionKind> =
S extends SelectionKind.Multiple ? string[] : string;
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[],
): Promise<QuickPickReturn<S>> {
// ...
}
这样,调用者可以正确地推断返回类型:
// `SelectionKind.Multiple` 返回 `string[]` - 正确 ✅
let shoppingList: string[] = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"],
);
// `SelectionKind.Single` 返回 `string` - 正确 ✅
let dinner: string = await showQuickPick(
"What's for dinner tonight?",
SelectionKind.Single,
["sushi", "pasta", "tacos", "ugh I'm too hungry to think, whatever you want"],
);
但当我们尝试实现showQuickPick
时,TypeScript会报错:
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[],
): Promise<QuickPickReturn<S>> {
if (items.length < 1) {
throw new Error("At least one item must be provided.");
}
let buttons = items.map(item => ({
selected: false,
text: item,
}));
if (selectionKind === SelectionKind.Single) {
buttons[0].selected = true;
}
const selectedItems = buttons
.filter(button => button.selected)
.map(button => button.text);
if (selectionKind === SelectionKind.Single) {
return selectedItems[0];
} else {
return selectedItems;
}
}
TypeScript会报错:
Type 'string[]' is not assignable to type 'QuickPickReturn<S>'.
Type 'string' is not assignable to type 'QuickPickReturn<S>'.
在TypeScript 5.8之前,我们需要使用类型断言来解决这个问题,但这会绕过TypeScript的类型检查,可能会导致错误。例如:
if (selectionKind === SelectionKind.Single) {
return selectedItems[0] as QuickPickReturn<S>;
} else {
return selectedItems as QuickPickReturn<S>;
}
TypeScript 5.8引入了一种新的检查机制,允许在返回语句中对条件类型进行有限的检查。当函数的返回类型是一个泛型条件类型时,TypeScript会使用控制流分析来推断泛型参数的类型,并将其应用于条件类型。
为了使这种检查生效,我们需要明确地定义条件类型的所有分支:
type QuickPickReturn<S extends SelectionKind> =
S extends SelectionKind.Multiple ? string[] :
S extends SelectionKind.Single ? string :
never;
这样,TypeScript可以正确地推断返回类型,并在调用者代码中提供类型安全性。如果我们在if
分支中交换了返回值,TypeScript会报错:
if (selectionKind === SelectionKind.Single) {
return selectedItems;
// error! Type 'string[]' is not assignable to type 'string'.
} else {
return selectedItems[0];
// error! Type 'string[]' is not assignable to type 'string'.
}
此外,TypeScript 5.8还支持在索引访问类型中使用类似的检查机制。例如,我们可以使用一个类型映射来定义返回类型:
interface QuickPickReturn {
[SelectionKind.Single]: string;
[SelectionKind.Multiple]: string[];
}
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[],
): Promise<QuickPickReturn[S]> {
// ...
}
这种方式在许多情况下更加简洁易用。
限制
这种检查机制有一些限制。它只在以下情况下生效:
- 返回类型是一个泛型条件类型或索引访问类型。
- 至少有两个分支,其中一个分支的返回类型是
never
。 - 泛型参数的约束是一个联合类型。
例如,以下代码可以触发这种检查:
function f<T extends A | B>(x: T):
T extends A ? string :
T extends B ? number :
never
如果使用了嵌套的类型参数,TypeScript可能不会应用这种检查。例如,如果我们将参数封装在一个选项对象中,TypeScript不会应用这种检查:
interface QuickPickOptions<S> {
prompt: string,
selectionKind: S,
items: readonly string[]
}
async function showQuickPick<S extends SelectionKind>(
options: QuickPickOptions<S>
): Promise<QuickPickReturn[S]> {
// narrowing won't work correctly here...
}
但可以通过编写一个条件类型来检查内部内容来解决这个问题。
在--module nodenext
下支持require()
ECMAScript模块
多年来,Node.js支持了ECMAScript模块(ESM)和CommonJS模块的互操作性。然而,这种互操作性存在一些限制:
- ESM文件可以
import
CommonJS文件。 - CommonJS文件不能
require()
ESM文件。
这意味着,如果库作者希望提供ESM支持,他们需要选择以下三种方案之一:
- 破坏与CommonJS用户的兼容性。
- 双向发布库(为ESM和CommonJS分别提供入口点)。
- 继续使用CommonJS。
双向发布虽然听起来是一个折中的方案,但它的过程复杂且容易出错,同时会显著增加包的代码量。
Node.js 22放宽了这些限制,允许CommonJS模块通过require()
调用ESM文件(但不支持包含顶层await
的ESM文件)。这为库作者提供了一个重大机会,使他们可以在不双向发布库的情况下提供ESM支持。
TypeScript 5.8在--module nodenext
标志下支持这种行为。启用--module nodenext
后,TypeScript将不会对require()
调用ESM文件报错。
由于此功能可能会被回溯到较旧版本的Node.js,因此目前没有稳定的--module nodeXXXX
选项启用此行为。然而,我们预计未来版本的TypeScript可能会在node20
下稳定此功能。在此期间,我们建议使用Node.js 22及更高版本的用户使用--module nodenext
,而库作者和使用较旧版本Node.js的用户应继续使用--module node16
(或升级到--module node18
)。
--module node18
TypeScript 5.8引入了一个稳定的--module node18
标志。对于那些固定使用Node.js 18的用户,这个标志提供了一个稳定的参考点,不会引入--module nodenext
中的一些行为。具体来说:
- 在
node18
下,require()
调用ESM文件是不允许的,但在nodenext
下是允许的。 - 在
node18
下,允许使用导入断言(已废弃,推荐使用导入属性),但在nodenext
下是不允许的。
更多详细信息可以参考--module node18
的拉取请求和对--module nodenext
的更改。
--erasableSyntaxOnly
选项
最近,Node.js 23.6取消了实验性支持直接运行TypeScript文件的标志。然而,在这种模式下,只支持某些构造。Node.js引入了一个名为--experimental-strip-types
的模式,要求任何TypeScript特有的语法不能具有运行时语义。换句话说,必须能够轻松地“擦除”或“剥离”文件中的任何TypeScript特有的语法,留下一个有效的JavaScript文件。
这意味着以下构造是不支持的:
enum
声明- 具有运行时代码的
namespace
和module
- 类中的参数属性
import
别名
类似的工具(如ts-blank-space或Amaro)也有相同的限制。这些工具会在遇到不符合要求的代码时提供有用的错误消息,但你仍然需要实际运行代码才能发现它不工作。
因此,TypeScript 5.8引入了--erasableSyntaxOnly
标志。启用此标志后,TypeScript只会允许使用可以被擦除的构造,并在遇到不能擦除的构造时报错:
class C {
constructor(public x: number) { }
// ~~~~~~~~~~~~~~~~
// error! This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
}
更多详细信息可以参考实现。
--libReplacement
标志
在TypeScript 4.5中,我们引入了用自定义文件替换默认lib
文件的可能性。这是基于从名为@typescript/lib-*
的包中解析库文件的可能性。例如,你可以通过以下package.json
将dom
库锁定到特定版本的@types/web
包:
{
"devDependencies": {
"@typescript/lib-dom": "npm:@types/web@0.0.199"
}
}
安装后,名为@typescript/lib-dom
的包应该存在,TypeScript在dom
被你的设置隐含时总会查找它。
这是一个强大的功能,但它也带来了一些额外的工作。即使你没有使用这个功能,TypeScript也会始终执行这个查找,并且需要监视node_modules
的变化,以防一个lib
替换包开始存在。
TypeScript 5.8引入了--libReplacement
标志,允许你禁用这种行为。如果你没有使用--libReplacement
,你可以通过--libReplacement false
禁用它。在未来,--libReplacement false
可能会成为默认值,因此如果你目前依赖这个行为,你应该考虑通过--libReplacement true
显式启用它。
更多详细信息可以参考更改。
在声明文件中保留计算属性名称
为了使类中的计算属性在声明文件中具有更可预测的输出,TypeScript 5.8将在类中一致地保留实体名称(如bareVariables
和dotted.names.that.look.like.this
)作为计算属性名称。
例如,考虑以下代码:
export let propName = "theAnswer";
export class MyClass {
[propName] = 42;
// error!
// A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.
}
在TypeScript 5.8之前,生成声明文件时会报错,并且生成的声明文件会包含一个索引签名:
export declare let propName: string;
export declare class MyClass {
[x: string]: number;
}
在TypeScript 5.8中,上述代码现在被允许,生成的声明文件将与你编写的代码一致:
export declare let propName: string;
export declare class MyClass {
[propName]: number;
}
请注意,这不会在类上创建静态命名的属性。你仍然会得到一个类似于[x: string]: number
的索引签名,因此对于这种用例,你需要使用unique symbol
或字面量类型。
请注意,编写这种代码在--isolatedDeclarations
标志下一直是错误的,但预计由于这一变化,计算属性名称将通常被允许在声明文件中使用。
请注意,有可能(尽管不太可能)TypeScript 5.8编译的文件生成的声明文件在TypeScript 5.7或更早版本中不向后兼容。
更多详细信息可以参考实现PR。
程序加载和更新的优化
TypeScript 5.8引入了许多优化,可以改善构建程序的时间,以及在--watch
模式或编辑器场景中基于文件更改更新程序的时间。
首先,TypeScript现在避免了在路径规范化中涉及的数组分配。通常,路径规范化会涉及将路径的每一部分分割成一个字符串数组,根据相对段规范化结果路径,然后使用规范分隔符将它们重新连接起来。对于包含许多文件的项目,这是一个显著且重复的工作量。TypeScript现在避免分配数组,而是直接在原始路径的索引上操作。
此外,当更改不会改变项目的根本结构时,TypeScript现在避免重新验证提供给它的选项(例如tsconfig.json
的内容)。这意味着,例如,简单编辑可能不需要检查项目的输出路径是否与输入路径冲突。相反,可以使用上次检查的结果。这应该使大型项目的编辑感觉更响应。
值得注意的行为变更
本节强调了一些值得注意的变更,这些变更应在升级时被识别和理解。有时它会突出显示弃用、移除和新限制。它也可以包含功能改进的错误修复,但这些修复也会影响现有构建,引入新错误。
lib.d.ts
DOM生成的类型可能会对代码库的类型检查产生影响。有关更多信息,请参阅与DOM和lib.d.ts
更新相关的问题。
在--module nodenext
下对导入断言的限制
导入断言是ECMAScript的一个提议,用于确保导入的某些属性(例如“这个模块是JSON,不是可执行JavaScript代码”)。它们被重新设计为一个名为导入属性的提议。作为过渡的一部分,它们从使用assert
关键字改为使用with
关键字。
// 一个导入断言 ❌ - 不兼容大多数运行时的未来版本。
import data from "./data.json" assert { type: "json" };
// 一个导入属性 ✅ - 导入JSON文件的推荐方式。
import data from "./data.json" with { type: "json" };
Node.js 22不再接受使用assert
语法的导入断言。因此,当在TypeScript 5.8中启用--module nodenext
时,TypeScript会在遇到导入断言时报错:
import data from "./data.json" assert { type: "json" };
// ~~~~~~
// error! Import assertions have been replaced by import attributes. Use 'with' instead of 'assert'
更多详细信息可以参考更改。
接下来会发生什么?
目前,TypeScript 5.8已经“功能稳定”。TypeScript 5.8的重点将是修复错误、打磨细节以及某些低风险的编辑器功能。我们将在接下来的几周内发布候选版本,随后很快就会发布稳定版本。如果你对规划版本发布感兴趣,请务必关注我们的迭代计划,其中包含目标发布时间等更多信息。
请注意:虽然测试版是尝试TypeScript下一个版本的好方法,但你也可以尝试夜间版本,以获取TypeScript 5.8的最新版本,直到我们发布候选版本。我们的夜间版本经过了充分测试,甚至可以仅在你的编辑器中使用。
因此,请今天尝试测试版或夜间版本,并告诉我们你的想法!
祝编程愉快!
——Daniel Rosenwasser和TypeScript团队
最后感谢阅读!欢迎关注我,微信公众号:倔强青铜三
。欢迎点赞
、收藏
、关注
,一键三连!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。