发布于 2026-01-06 0 阅读
0

TypeScript 中从数组创建联合体的两种方法

TypeScript 中从数组创建联合体的两种方法

假设我们有一个简单的接口来描述一个对象。

interface Person {
    name: string;
    age: number;
    address: string;
};

// This is a valid 'Person' since the object contains all of the
// keys from the interface above and the types allline up.
const person: Person = {name: "shane", age: 21, address: "England"}
Enter fullscreen mode Exit fullscreen mode

但有时,我们可能需要基于旧接口推导出新接口,但新接口仅限于某些键。

我们经常在 API 响应中这样做,以限制返回的数据。

举个简单的例子,我们来看看如何创建一个只包含接口中name&age字段的新接口Person

派生一种新类型

当然,在这样一个只有 3 个字段的简单例子中,我们可以而且应该直接创建一个像这样的新接口:

interface Person2 {
    name: string;
    age: number;
};
Enter fullscreen mode Exit fullscreen mode

但我们都知道,在实际代码库中,接口会变得非常非常大——所以我们真的希望能够重用这些name字段age及其类型。

一种方法是保留pick你想要保留的字段。

type Person2 = Pick<Person, "name" | "age">
Enter fullscreen mode Exit fullscreen mode

这正是我们上面想要的结果,因为Pick它利用联合体的元素"name" | "age"创建了一个新的映射类型。这和我们手动编写以下代码的效果是一样的:

type Person2 = {[K in "name" | "age"]: Person[K]}
Enter fullscreen mode Exit fullscreen mode

这里真正重要的是,我们是在派生一种新类型,而不是硬编码一种新类型。这意味着对底层Person接口的后续更改将反映到我们整个代码库中。

例如,如果属性从 a变为 an user则该更改将传播到我们的新类型。PersonobjectstringPerson2

映射类型真是太棒了

正如你从上面所看到的,从现有类型派生新类型是一个非常强大的概念——随着你 Typescript 知识的增加,你会越来越多地使用它。

现在,回顾上面的代码示例,如果想"name" | "age"用动态数组替换联合体会发生什么情况?

你可以想象一个函数,它接受一个字符串形式的键数组,并返回一个只包含这些键的对象。

// Here's the goal: we want Typescript
// to know that `p` here is not only a subset of `Person`
// but also it ONLY contains the keys "name" & "age"
const p = getPerson(["name", "age"]);
Enter fullscreen mode Exit fullscreen mode

第一种方法可能是接受string[]字段值,然后返回一个值。Partial<Person>

function getPerson(fields: string[]): Partial<Person> {
   // implementation omitted for brevity
   return {} as any;
}
Enter fullscreen mode Exit fullscreen mode

这样做存在两个主要问题:

  1. 这里的参数fields可以包含任何字符串,但我们希望将其缩小范围,只允许来自Person接口的键。
  2. 返回类型Partial<Person>会丢失类型信息,因为它将所有字段都设为可选。

先硬编码。

为了理解如何解决这两个问题,让我们重写函数签名,使其包含我们在示例中想要的类型。

function getPerson(fields: ("name" | "age")[]): Pick<Person, "name" | "age"> {
   // implementation omitted for brevity
   return {} as any;
}
Enter fullscreen mode Exit fullscreen mode

虽然现在有点混乱,但fields首先看看这个论证,你会发现这并不是string[]我们想要的(因为这种类型包含所有字符串),它实际上只是“一个存在于个人中的键的数组”。

本文旨在讲解 TypeScript 如何创建和使用联合类型,以及如何将它们与数组类型结合起来。

// This is a "union" of 3 string literals
type KeyUnion = ("name" | "age" | "person");

// This is an Array type, where it's elements are 
// restricted to those that are found in the union 
// "name" | "age" | "person"
type KeyArray = ("name" | "age" | "person")[];

// Sometimes it's easier to understand in this style
type KeyArray = Array<"name" | "age" | "person">;

// Finally you can substitute the strings for the
// type alias created above
type Keys = KeyUnion[]
Enter fullscreen mode Exit fullscreen mode

这是对一些基本联合体/数组类型的一个很好的入门介绍——理解接下来的内容需要这些知识。

不过现在,我们希望完全避免使用硬编码的字符串字面量。

维护一个引用接口键的字符串列表会非常繁琐,幸运的是 Typescript 提供了许多便捷类型来帮助我们解决这个问题。

// Before: hard-coded keys
type KeyUnion = ("name" | "age" | "person");

// After: a 'derived' union type based on 
// the Person interface
type KeyUnion = keyof Person;
Enter fullscreen mode Exit fullscreen mode

虽然这两个函数等效,但很明显哪个更合适。因此,将这一部分应用到我们的函数中,我们得到以下结果:

function getPerson(fields: (keyof Person)[]): Pick<Person, keyof Person> {
   // implementation omitted for brevity
   return {} as any;
}
Enter fullscreen mode Exit fullscreen mode

还没到那一步。

这确实为函数调用点提供了类型安全——你将无法getPerson再使用接口中不存在的键进行调用。

// Type 'string' is not assignable to type '"name" | "age" | "address"'
getPerson(["name", "age", "location"])
Enter fullscreen mode Exit fullscreen mode

如上所示,TS 正确地阻止了这种情况,因为数组中的第 3 个元素不是有效的键Person

但是,返回类型现在已关闭。

如果我们稍作回顾mapped types,就会很容易明白为什么<Pick, keyof Person>这不是我们想要的。

// this creates a new type with ALL the keys in Person
Pick<Person, keyof Person>

// it's exactly the same as this, it's a 1:1 mapping
{[K in keyof Person]: Person[K]}

// Or, for even more clarity (not valid typescript though), it's like doing this
{[for K in "name" | "age" | "person"]: Person[K]}
Enter fullscreen mode Exit fullscreen mode

所以现在很清楚了,我们仍然想要实现某个功能Pick,但我们需要基于传入的键数组创建一个联合类型——本质上我们需要以下代码……

const keys = ["name"]
// -> should produce Pick<Person, "name">

const keys = ["name", "age"]
// -> should produce Pick<Person, "name" | "age">

const keys = ["name", "age", "address"]
// -> should produce Pick<Person, "name" | "age" | "address">
Enter fullscreen mode Exit fullscreen mode

从数组创建联合体的第一种方法

举个独立的例子,如果我们给出像这样的类型断言,就可以轻松地从以下代码中获得字符串字面量的并集:

const keys: (keyof Person)[] = ["name", "age"]
Enter fullscreen mode Exit fullscreen mode

(keyof Person)[]通过使用(而不是仅仅使用)来限制类型,string[]这意味着我们可以很好地要求 Typescript 从中派生出联合体。

// creates the union ("name" | "age")
type Keys = typeof keys[number]
Enter fullscreen mode Exit fullscreen mode

事实证明,typeof keys[number]任何数组使用 `union` 操作都会强制 TypeScript 生成数组中所有可能类型的联合体。

在我们的例子中,由于我们指定数组只能包含接口中的键,Typescript 会利用这一信息为我们提供所需的联合体。

为了更清楚地说明,以下方法行不通。

const keys = ["name", "age"]

// only creates the union `string` since there's no 
// type assertion after `keys` 
type Keys = typeof keys[number]
Enter fullscreen mode Exit fullscreen mode

第二种从数组创建联合体的方法

TypeScript 中的联合类型确实是解锁许多高级用例的关键,因此始终值得探索如何使用它们。

为了完成上面的函数,我们实际上会采用第一个例子,但了解这个技巧也很有用,以防你遇到类似的情况。

常量断言

利用一项相对较新的功能,您可以指示 TS 将数组(和其他类型)视为字面量(只读)类型。

你看,Typescript 推断keys下面的变量具有技术上正确的类型string[],但对于我们的用例来说,如果我们从中创建一个联合体,这意味着会丢失有价值的信息。

// to TS, this is just `string[]`...
const keys = ["name", "age"];

// ... which means this union 
// type ends up with just `string`, 
// which includes every single string ever :(
type Keys = typeof keys[number];
Enter fullscreen mode Exit fullscreen mode

但是,使用 `{{ type="type">` const assertion,我们会得到截然不同的结果。你实际上是在告诉 TypeScript 不允许扩展任何字面类型。

所以"name"它仍然是"name",而不是string——这意味着,如果我们回到前面的例子,我们现在可以要求 Typescript 创建所有元素的联合。

// now, TS thinks this is `readonly ["name", "age"]`
const keys = ["name", "age"] as const;

// this is now the union we wanted, "name" | "age"
type Keys = typeof keys[number];
Enter fullscreen mode Exit fullscreen mode

完成实施

最后,让我们把这些知识付诸实践。

总而言之,我们希望使用 TypeScript 来注解一个函数,以便在使用字符串数组调用该函数时,它能够验证所有字符串是否都与接口上的键名匹配,然后使用这些动态值来创建一个从原始接口派生的缩小的返回类型。

// only accept keys from `Person`
// then use them to narrow a new type. 
function getPerson<T extends (keyof Person)[]> (fields: T): Pick<Person, T[number]> {
   // implementation omitted for brevity
   return {} as any;
}
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/shakyshane/2-ways-to-create-a-union-from-an-array-in-typescript-1kd6