TypeScript 5.4 Beta 中的新增功能

2024-02-13 08:22:17 浏览数 (2)

TypeScript 5.4 Beta 刚刚发布,带来了一些令人兴奋的新功能,同时修复了一些错误并改进了一些用户体验。毫不拖延,让我们快速探索一下这些重大改进。

Object.groupBy 和 Map.groupBy

TypeScript 5.4 Beta 中添加的一个新的 API 改变是对即将到来的 JavaScript 方法 Object.groupBy 和 Map.groupBy 的声明。这些静态方法极大地简化了在数组(以及对象或地图等可迭代对象)中对项目进行分组的操作。

它通过接受一个可迭代对象和一个分类每个元素应该被放置在哪个组中的函数来工作。然后,该函数的结果被用来为每个不同的组创建一个对象键,并将原始元素添加到每个键的数组中。以下是一个示例:

代码语言:typescript复制
const people = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 42 },
  { name: "Charlie", age: 60 },
  { name: "David", age: 30 },
  // ... 更多人员
];

// 按年龄范围对人员进行分组
const ageGroups = Object.groupBy(people, (person) => {
  if (person.age < 30) return "young";
  else if (person.age >= 30 && person.age < 60) return "adult";
  else return "senior";
});

上述代码的结果与下面的代码等价:

代码语言:typescript复制
const people = {
  young: [{ name: 'Alice', age: 25 }, { name: 'David', age: 30 }],
  adult: [{ name: 'Bob', age: 42 }],
  senior: [{ name: 'Charlie', age: 60 }]
};

而且,这也可以用于其他可迭代对象,比如数组或地图。

对于 Map.groupBy,它的表现与 Object.groupBy 相同,但是它产生的是一个地图而不是普通对象。

代码语言:typescript复制
const fruits = ['apple', 'banana', 'orange', 'kiwi'];

// 按第一个字母将水果分组
const letterGroups = Map.groupBy(fruits, (fruit) => fruit.charAt(0));

// 结果地图:
// Map {
//   'a' => ['apple'],
//   'b' => ['banana'],
//   'o' => ['orange'],
//   'k' => ['kiwi']
// }

需要注意的是,生成的对象最终成为了一个 Partial 记录,因为编译器无法确保所有键都被创建。要访问变量,您必须使用可选链操作符或检查是否为 undefined。

代码语言:typescript复制
type AgeGroup = Partial<
  Record<"young" | "adult" | "senior", { name: string; age: number }[]>
>;

ageGroups?.young // 正确
ageGroups.young && ageGroups.young[0].age; // 正确
ageGroups.young[0].age; // 错误:对象可能为 'undefined'

该静态方法尚未包含在标准中,因为它是一个待定的 TC39 提案。然而,它处于第 4 阶段,表明它将被包含在下一个稳定版本 ES2024 中。要使用这些方法,您必须在 tsconfig 设置中将目标和 lib 更改为 ESNext。

NoInfer 实用类型

长期以来,存在这样一种情况:您有一个具有多个参数或相同类型参数的属性的通用函数,但不想将所有类型推断到通用值。这个实用类型恰好解决了这个问题,提供了对推断类型的更多控制。

让我们考虑一个接收值列表的函数,例如这个示例中的水果,以及一个默认值。

代码语言:typescript复制
declare function getValue<T>(values: T[], defaultValue: T): T;

// 示例:没有 NoInfer<T>
const result = getValue(["apple", "lemon"], "apple"); // 正确

对于这个示例,TypeScript 推断出 result 的类型为 "apple" | "lemon" ,正如应该的那样。但是,如果我们将默认值更改为非常不同的内容呢?

代码语言:typescript复制
const result = getValue(["apple", "lemon"], "bomb"); // 也是正确的

目前,推断的结果是 "apple" | "lemon" | "bomb"。但您

可能会想,为什么会这样?我们的 "values" 参数不是应该是我们的真相之源,允许我们从中选择一个初始值吗?确实应该如此,但存在微妙的细微差别。由于两者都共享相同的通用类型,"bomb" 被视为一个有效的推断候选项,类似于值列表 T。简单来说,TypeScript 将 defaultValue 的值推断为 fruits T 的联合。

解决此问题的一种常见方法是添加一个扩展我们预期类型参数的不同类型参数。

代码语言:typescript复制
declare function getValue<const T, U extends T>(values: T[], defaultValue: U): T;

const result = getValue(["apple", "lemon"], "bomb");
// 错误:类型 "bomb" 的参数不能赋值给类型 ("apple" | "lemon") 的参数

这也可以工作,但它更加冗长,并且在签名中 D 可能不会在其他地方使用。

这就是新的实用类型 NoInfer 的用处。通过将我们的类型包围在 NoInfer<...> 中,TypeScript 将跳过将类型参数添加为类型推断候选项。

代码语言:typescript复制
declare function getValue<const T>(values: T[], defaultValue: NoInfer<T>): T;

// 示例:使用 NoInfer<T>
const result = getValue(["apple", "lemon"], "bomb");
// 错误:类型 "bomb" 的参数不能赋值给类型 ("apple" | "lemon") 的参数

通过排除 defaultValue 类型,我们确保输入的任何内容都不包含在函数返回或推断的值的联合中。

在这个实用类型正式引入之前,社区已经创建了一个解决此问题的临时类型。

代码语言:typescript复制
type NoInfer<T> = T & {[K in keyof T]: T[K]};

尽管与我们今天拥有的内置实用程序类型相比,它的性能效率稍逊一筹,主要是因为 TypeScript 需要深度探索复杂类型,以找到推断候选项。这是引发此更改的 GitHub 问题的参考。

总结一下,TypeScript 5.4 Beta 引入了重大改进,其中一个我忘记提到的是:在闭包中保留缩小范围。这允许在函数内更准确地缩小类型,解决了类型检查中的一个常见痛点。这只是引入的几个值得注意的变化之一。要获得更全面的概述,请参阅官方发布说明。

希望您觉得这篇文章有用。如果您喜欢,请给个赞。

0 人点赞