从联合类型中提取,其中鉴别器也是联合

Jus*_*dei 5 types type-conversion typescript typescript-generics

我有这样的类型:

enum Type {
  A = 'A',
  B = 'B',
  C = 'C'
}

type Union =
 | {
     type: Type.A | Type.B;
     key1: string
   }
 | {
     type: Type.C;
     key2: string
   }

type EnumToUnionMap = {
  [T in Type]: {
    [k in keyof Extract<Union, {type: T}>]: string
  }
}
Run Code Online (Sandbox Code Playgroud)

我遇到的问题typeof EnumToUnionMap[Type.A]never(实际上,它是一个通用的键签名,[x: string]: string但那是因为当是或时Extract<Union, {type: T}>返回类型)而是neverTType.AType.Btypeof EnumToUnionMap[Type.C]

{
  type: Type.C,
  key2: string
}
Run Code Online (Sandbox Code Playgroud)

正如预期的那样。

这一切都是有道理的,因为typeinEnumToUnionMap[Type.A]Type.A | Type.BType.A != (Type.A | Type.B)所以它们不匹配,我们得到never.

基本上我需要做这样的事情:

type EnumToUnionMap = {
  [T in Type]: {
    [k in keyof Extract<Union, T in Union['type']>]: string
  }
}
Run Code Online (Sandbox Code Playgroud)

为什么我需要这样做:

我收到来自具有以下形状的警报端点的响应:

{
 type: Type,
 key1: string,
 key2: string
}
Run Code Online (Sandbox Code Playgroud)

Type.AType.B提供的警报key1同时Type.C提供key2
我需要将响应中的键映射到网格中的列名(其中某些警报类型共享一组通用键,但列的显示名称不同):

const columnMap: EnumToUnionMap = {
  [Type.A]: {
    key1: 'Column name'
    // Note that in actuality this object will contain
    // multiple keys (i.e. multiple columns) for a
    // given `Type` so creating a map
    // between `Type -> column name` is not possible.
  },
  [Type.B]: {
    key1: 'Different column name'
  },
  [Type.C]: {
    key2: 'Another column name'
  }
}
Run Code Online (Sandbox Code Playgroud)

这样,我可以执行以下操作:

const toColumnText = (alert) => columnMap[alert.type]

...

if (alert.type === Type.A) {
  const key1ColumnName = toColumnText(alert).key1 // typed as string
  const key2ColumnName = toColumnText(alert).key2 // Typescript warns of undefined key
}
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 4

正如您所注意到的,您实际上不能在这里使用实用Extract程序类型,因为联合成员Union与您拥有的候选类型之间的关系{type: T}不是简单的可分配性。相反,您想要找到满足以下条件的U联合成员。您可能不得不忘记 TypeScript 提供的实用程序类型,而是自己执行类型操作。UnionT extends U["type"]


一种可能的定义EnumToUnionMap是:

type EnumToUnionMap = { [T in Type]: Union extends infer U ? U extends { type: any } ? (
    T extends U["type"] ? { [K in keyof U as Exclude<K, "type">]: U[K] } : never
) : never : never }
Run Code Online (Sandbox Code Playgroud)

这可能看起来有点令人畏惧;让我们确保它至少能满足您的要求。IntelliSense 显示它的计算结果为:

/* type EnumToUnionMap = {
    A: { key1: string; };
    B: { key1: string; };
    C: { key2: string; };
} */
Run Code Online (Sandbox Code Playgroud)

看起来不错。


现在我们知道它可以满足您的要求,那么它是如何做到的呢?让我们将定义分成几部分并分析每一部分:

type EnumToUnionMap = { [T in Type]: Union 扩展推断 U ? U 扩展 { 类型:任意 } ?(
    T 扩展 U[“类型”] ?{ [K in keyof U as Exclude]: U[K] } : 从不
):从不:从不}

正如在您的版本中一样,我们正在映射枚举值Type.AType.BType.C。对于每个这样的枚举值T,我们需要分成Union它的联合成员,并收集我们关心的成员。写入Union extends infer U ? U extends { type: any } ? ...使用条件类型推断复制Union到新的类型参数中U,然后可以使用该参数创建分布式条件类型,其中U表示 的各个联合成员Union。因此,从现在开始,每当我们看到时U,我们所处理的只是事物的片段Union,而不是整个事物。

type EnumToUnionMap = { [T in Type]: Union 扩展推断 U ? U 扩展 { 类型:任意 } ?(
     T extends U["type"] ? { [K in keyof U as Exclude]: U[K] } : 从不
):从不:从不}

为了确定选择U联合体的哪个联合体成员,我们评估条件类型。当且仅当的属性是 的超类型时,该检查为 true 。如果是且是,则这是真的。但如果is且is ,则这是错误的。通过在检查为假时返回,我们将只处理 的“正确”成员。UnionT extends U["type"] ? ... : neverT extends U["type"]UtypeTTType.AU{type: Type.A | Type.B, ...}TType.AU{type: Type.C, ...}neverUnion

type EnumToUnionMap = { [T in Type]: Union 扩展推断 U ? U 扩展 { 类型:任意 } ?(
    T 扩展 U[“类型”] ?{ [K in keyof U as Exclude]: U[K] } : 从不
):从不:从不}

所以我们找到了正确的 union member U,但我们不想只是返回它,因为它仍然包含不需要的type属性。我们可以Omit<U, "type">使用实用程序类型返回Omit,但不幸的是,这会导致冗长的 IntelliSense,Omit<{type: Type.A | Type.B; key1: string}, "type">而不是理想的结果{key1: string}。因此,在这里我使用映射类型中的键重新映射Omit来编写自己的类型(文档解释了这种替代方法的工作原理)。Omit


就这样吧;该EnumToUnionMap类型遍历 的每个成员Type,提取Uniontype属性是其超类型的成员,并忽略type该成员的属性。

Playground 代码链接