• TypeScript 5.8 beta 版本给我们带来了哪些新特性?
  • 发布于 1个月前
  • 119 热度
    0 评论
  • Cactus
  • 20 粉丝 46 篇博客
  •   
TypeScript 最近刚刚发布了 5.8 beta 版本,我们一起来看看还有哪些值得关注的新特性。

条件类型和索引访问类型的返回检查
假设我们有一个函数 showQuickPick,它根据用户的选择返回单个字符串或字符串数组。函数的返回类型依赖于参数 selectionKind,当 selectionKind 为 SelectionKind.Single 时,返回类型应为 string;当为 SelectionKind.Multiple 时,返回类型应为 string[]。在之前的 TypeScript 版本中,函数的返回类型被定义为 Promise<string | string[]>,这意味着调用者需要手动检查返回值的类型,增加了代码的复杂性。例如:
let shoppingList = await showQuickPick(
    "Which fruits do you want to purchase?",
    SelectionKind.Multiple,
    ["code秘密花园", "ConardLi", "bananas", "durian"],
);
 // 堆代码 duidaima.com
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
// 错误:`join` 方法不存在于 `string | string[]` 类型
TypeScript 5.8 引入了更智能的条件类型推断机制。通过定义条件类型 QuickPickReturn,我们可以让函数的返回类型根据 selectionKind 动态变化:
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>> {
    // 实现代码
}
这样一来,调用者无需手动检查类型,TypeScript 会自动推断出正确的返回类型:
// 返回类型为 string[]
let shoppingList: string[] = await showQuickPick(
    "Which fruits do you want to purchase?",
    SelectionKind.Multiple,
    ["code秘密花园", "ConardLi", "bananas", "durian"],
);

// 返回类型为 string
let dinner: string = await showQuickPick(
    "What's for dinner tonight?",
    SelectionKind.Single,
    ["code秘密花园", "ConardLi", "tacos", "ugh I'm too hungry to think, whatever you want"],
);
在实现 showQuickPick 函数时,TypeScript 5.8 还改进了对条件类型的控制流分析。例如,当 selectionKind 为 SelectionKind.Single 时,TypeScript 能够自动推断出返回类型应为 string,而无需手动添加类型断言:
if (selectionKind === SelectionKind.Single) {
    return selectedItems[0]; // 自动推断为 string
} else {
    return selectedItems; // 自动推断为 string[]
}
如果开发者不小心写错了分支逻辑,TypeScript 也会及时报错,避免潜在的类型问题:
if (selectionKind === SelectionKind.Single) {
    return selectedItems; // 错误:返回类型应为 string,而不是 string[]
} else {
    return selectedItems[0]; // 错误:返回类型应为 string[],而不是 string
}
支持 require() 导入 ECMAScript 模块
多年来,Node.js 一直支持 ECMAScript 模块(ESM)和 CommonJS 模块并存。然而,这两者之间的互操作性存在一些问题:
1.ESM 文件可以导入 CommonJS 文件

2.CommonJS 文件不能 require() ESM 文件


这意味着从 ESM 文件中消费 CommonJS 文件是可行的,但反过来却不行。这给希望提供 ESM 支持的库作者带来了很多挑战。他们要么不得不放弃对 CommonJS 用户的兼容性,要么选择“双重发布”他们的库(为 ESM 和 CommonJS 提供单独的入口点),或者干脆一直停留在 CommonJS 上。虽然双重发布听起来是一个折中的好办法,但它是一个复杂且容易出错的过程,同时也大大增加了包内代码的数量。

Node.js 22 放宽了一些限制,允许从 CommonJS 模块 require("esm") 调用 ECMAScript 模块。虽然 Node.js 仍然不允许对包含顶级 await 的 ESM 文件使用 require(),但大多数其他 ESM 文件现在可以从 CommonJS 文件中消费。这为库作者提供了一个重大机会,可以在不进行双重发布的情况下提供 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 nodenext 和 --module node18 的区别:
1.在 node18 下,require() ECMAScript 模块是不允许的,而在 nodenext 下是允许的。
2.在 node18 下,导入断言(已被导入属性取代)是允许的,而在 nodenext 下是不允许的。
示例:
// 使用 `--module nodenext` 时,以下代码不会报错
const esmModule = require('./ConardLi.mjs');
--erasableSyntaxOnly 选项
最近,Node.js 23.6 取消了对直接运行 TypeScript 文件的实验性支持,但仅支持某些特定的语法结构。Node.js 引入了一个名为 --experimental-strip-types 的模式,要求任何 TypeScript 特有的语法都不能有运行时语义。换句话说,必须能够轻松地从文件中删除任何 TypeScript 特有的语法,留下一个有效的 JavaScript 文件。

这意味着以下结构是不支持的:
1.enum 声明
2.含有运行时代码的命名空间和模块
3.类中的参数属性

4.导入别名


类似的工具如 ts-blank-space 或 Amaro(Node.js 中用于类型剥离的底层库)也有相同的限制。这些工具在遇到不符合要求的代码时会提供有用的错误信息,但你仍然不会知道你的代码是否能正常运行,直到你实际尝试运行它。为了解决这个问题,TypeScript 5.8 引入了 --erasableSyntaxOnly 标志。当启用该标志时,TypeScript 只允许使用可以从文件中删除的语法结构,并在遇到任何不可删除的语法结构时发出错误。

以下是一个启用了 --erasableSyntaxOnly 标志后的示例:
class C {
    constructor(public x: number) { }
    //          ~~~~~~~~~~~~~~~~
    // error! 当启用 'erasableSyntaxOnly' 时,不允许使用这种语法。
}
通过启用 --erasableSyntaxOnly,开发者可以确保他们的 TypeScript 代码在 Node.js 中直接运行时不会遇到不支持的语法结构。这不仅提高了代码的兼容性,还减少了在运行时遇到错误的可能性。

声明文件中的计算属性名保留
在之前的 TypeScript 版本中,计算属性名在生成声明文件时可能会出现不一致的问题。具体来说,当在类中使用计算属性名时,TypeScript 可能会发出错误,或者生成的声明文件与实际代码不匹配。这给开发者带来了困扰,因为声明文件的准确性对于类型检查和代码提示至关重要。TypeScript 5.8 对此进行了改进,现在会在声明文件中保留计算属性名,使其与实际代码保持一致。这一改进使得计算属性名在声明文件中的表现更加可预测,提升了声明文件的准确性。

以下是一个示例代码,展示了计算属性名在声明文件中的处理:
export let propName = "ConardLi";

export class MyClass {
    [propName] = 42;
    //  ~~~~~~~~~~
    // error!
    // 在类属性声明中,计算属性名必须是简单的字面量类型或 'unique symbol' 类型。
}
在之前的 TypeScript 版本中,生成的声明文件可能会包含一个索引签名,如下所示:
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 或字面量类型。

其他更新
除了主要的新特性,TypeScript 5.8 还带来了一些其他重要的改进和优化,提升了整体开发体验。首先,程序加载和更新方面进行了多项优化,例如路径规范化过程中避免了数组分配,直接操作路径索引,减少了处理大量文件时的重复工作;在编辑过程中,如果项目的基本结构没有变化,TypeScript 将避免重新验证配置选项,从而提高了大项目的编辑响应速度。

另外还引入了一些显著的行为变化,如 DOM 类型的更新可能会影响代码库的类型检查,导入断言已被导入属性取代,在启用 --module nodenext 时,TypeScript 将对使用 assert 语法的导入断言发出错误。其他改进还包括引入 --libReplacement 标志,允许开发者禁用默认的库文件替换行为,以及在声明文件中保留计算属性名,使其与实际代码保持一致,提升了声明文件的准确性。

详情请了解:https://devblogs.microsoft.com/typescript/announcing-typescript-5-8-beta/
用户评论