Typescript 通过鉴别器 属性 将已鉴别联合类型的数组分组到记录中

Typescript group an array of a discriminated union type into a record by a discriminator property

我正在尝试编写通用“groupBy”函数的打字稿签名,该函数会将已区分类型的联合数组“传播”到记录中,其中记录的每个字段都是一个可能的鉴别器值并指向一个数组来自联合的具体类型的对象。

示例:

interface Dog {
  type: 'dog'
  dogMetadata: {}
}

interface Cat {
  type: 'cat'
  catMetadata: {}
}

type Animal = Dog | Cat

const animals: Animal[] = [{ type: 'dog', dogMetadata: {} }, { type: 'cat', catMetadata: {} }]

每个接口都有一个共同的鉴别器属性,没有其他共同的属性。

这是一个简单的“groupBy”签名,它不传播类型联合值,迫使我向下转换记录的值:

function groupBy<T, K extends string>(arr: T[], keyExtractor: (element: T) => K): Record<K, T[]>

const animalsByType: Record<'dog' | 'cat', Animal[]> = groupBy(animals, it => it.type)
const dogs: Dog[] = animalsByType['dog'] as Dog[]  // Must downcast Animal[] to Dog[]

我怎样才能创建一个知道被区分联合类型的具体类型的“groupBy”?我想要这样的东西:

const animalsByType: { dog: Dog[], cat: Cat[] } = groupBy(animals, it => it.type)
const dogs: Dog[] = animalsByType['dog']  // animalsByType.dog is known to be Dog[] by typescript

实现很简单,Typescript 部分有问题 :) 我正在寻找一个不做假设的通用解决方案,例如鉴别器的名称 属性 或类型的数量类型联合。

后续问题

当联合嵌套在另一个 class 中时,是否可以使相同的签名起作用?

interface Holder<T> {
  data: T
}

const animalHolders: Holder<Animal>[] = animals.map(data => ({ data }))

const dogHolders: Holder<Dog> = groupBy(animalHolders, it => it.data.type) // Any way of doing this?

Playground link

感谢您的帮助。

好问题...

让我们首先创建一些实用程序类型:

type KeysOfType<O, T> = {
  [K in keyof O]: O[K] extends T ? K : never;
}[keyof O];

这会提取 O 中所有指向 T 类型值的键。这将用于将判别式的类型限制为 string 类型。它们将在您的输出类型中用作键,因此我们对允许其他类型的判别式并不感兴趣。

我们还要添加 以使我们的结果类型在智能感知中看起来更好。

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

现在,让我们创建一个类型来表示 groupBy 函数的 return-type:

type Return<T, K extends KeysOfType<T, string>> = 
    { [KK in string & T[K]]: { [_ in K]: KK } & T }

或者,可选地,自由地应用上面的 Expand<T> 类型,以便为消费者提供更好的智能感知:

type Return<T, K extends KeysOfType<T, string>> = 
    Expand<{ [KK in string & T[K]]: Expand<{ [_ in K]: KK } & T> }>    

现在我们可以声明函数了:

function groupBy<T, K extends KeysOfType<T, string>>(
    arr: T[], 
    keyExtractor: (element: T) => T[K]): Return<T, K>{
    throw Error();
}

并称它为:

const groups = groupBy(animals, e => e.type)

对于完整 type-safety,无论选择哪个鉴别器 属性。

Playground Link

有一个相当简单的解决方案,它利用条件类型分布在联合上的事实来摆脱 non-matching 替代方案:

type GroupBy<T extends Record<D, PropertyKey>, D extends keyof T> =
  {[K in T[D]]: T extends Record<D, K> ?  T[] : never}

declare function groupBy<T extends Record<D, PropertyKey>, D extends keyof T>
  (arr: T[], keyExtractor: (element: T) => T[D]): GroupBy<T, D>

它适用于原始示例,但也适用于其他区分键:

interface Orange { color: 'orange', juiceContent: number }
interface Banana { color: 'yellow', length: number}
type Fruit = Orange | Banana
const fruits: Fruit[] = [{color: 'orange', juiceContent: 250},{ color: 'yellow', length: 20}]
const fruitsByColor = groupBy(fruits, it => it.color)
const yellows = fruitsByColor.yellow
// const yellows: Banana[]

TypeScript playground