typescript 从其他联合类型派生联合类型

wyyhbhjk  于 2023-04-22  发布在  TypeScript
关注(0)|答案(2)|浏览(152)

假设我有一个这样的联合类型:

{id: 'A', format: 'foo' | 'bar, value: 1 | 2} 
  | 
{id: 'B', format: 'baz', value: 3 | 4} 
  | 
{id: 'C', format: 'qux', value: 'hello' | 'goodbye'}

我希望这种类型的每个对象都保持其内部关系,以便如果id等于A,则格式必须是foobar等。
现在,我想创建另一个与Entry相关的类型,它具有相同的id属性,但是format是可选的,value应该省略。我们称之为EntryInput(想象一个函数,你提供一个EntryInput作为输入参数,并返回一个Entry)。相反,我想从Entry类型创建它。然而,一旦你在这样的联合体上应用Pick,所有属性都会混合在一起形成{id: 'A' | 'B' | 'C'; format: 'foo' | 'bar' | 'baz' | 'qux'; value: 1 | 2 | 3 | 4 | 'hello' | 'goodbye'}。例如,请参见https://github.com/microsoft/TypeScript/issues/28339
我知道我可以创建一个泛型类型Entry<Id extends 'A' | 'B' | 'C'>并创建一个类似createEntry<Id>(input: EntryInput<Id>): Entry<Id>的函数来维护函数的输入类型和输出类型之间的关系,其中Id是从提供的值推断出来的,因此强制format正确地与id相关,但我需要的不是泛型类型,我需要一个联合类型,这样我就可以声明

const entries: Entry[] = [ ... ]

...并且这里的每个条目都是一致的,因此id的值决定了formatvalue的可能值。如果对泛型类型这样做,属性将再次分布,因此{id: 'A', format: 'qux', value: 'hello}是有效的Entry
如何设置类型,以便从Entry派生类型EntryInput,同时保持属性之间的内部关系不变?

cbeh67ev

cbeh67ev1#

从以下联合类型开始:

type Entry =
  { id: 'A', format: 'foo' | 'bar', value: 1 | 2 } |
  { id: 'B', format: 'baz', value: 3 | 4 } |
  { id: 'C', format: 'qux', value: 'hello' | 'goodbye' }

我们可以通过使用分配条件类型对其进行操作来创建所需形式的新并集,如下所示:

type EntryInput = Entry extends infer E ?
  E extends { id: infer I, format: infer F } ? { id: I, format?: F } : never
  : never;

/* type EntryInput = 
   { id: "A"; format?: "foo" | "bar" | undefined; } | 
   { id: "B"; format?: "baz" | undefined; } | 
   { id: "C"; format?: "qux" | undefined; } 
*/

条件类型仅在被检查的类型是generic类型参数时分布在联合上,Entry不是泛型。如果你尝试Entry extends ⋯ ? ⋯ : never,它不会分发。Entry2定义使用了一个小技巧,条件类型infer可以“复制”Entry转换为泛型类型参数E,这样检查E extends ⋯ ? ⋯ : never确实是分布式的。
(If这个技巧太奇怪了,你可以重构成

type EntryInputHelper<E> =
  E extends { id: infer I, format: infer F } ? { id: I, format?: F } : never
type EntryInput = EntryInputHelper<Entry>;

在这里,你创建一个泛型类型函数,只使用它一次。)
Playground链接到代码

kmbjn2e3

kmbjn2e32#

我自己的解决方案是这样的。我需要的是id到{format,value}的Map,以使用Map类型派生其他类型,这将分布在键上:

/**
 * This is just an internal type building the possible combinations, which the actual types will derive from
 */
type EntryMap = {
  A: {format: 'foo' | 'bar', value: 1 | 2},
  B: {format: 'baz', value: 3 | 4},
  C: {format: 'qux', value: 'hello' | 'goodbye'},
}

/**
 * Possible values for 'id'
 */
export type EntryId = keyof EntryMap;

/**
 * Combine id: 'A' | 'B' | 'C' with the properties of A, B or C, respectively, distributing over 'A' | 'B' | 'C'
 */
export type Entry<K extends EntryId = EntryId> = {
  [P in K]: {id: P} & EntryMap[P];
}[K];

/**
 * Combine id: 'A' | 'B' | 'C' with the optional format property of A, B or C, respectively, distributing over 'A' | 'B' | 'C'
 */
export type EntryInput<K extends EntryId = EntryId> = {
  [P in K]: {id: P} & Partial<Pick<EntryMap[P], 'format'>>;
}[K];

// tests
const e1: Entry = {id: 'A', format: 'foo', value: 1}; // OK
const e2: Entry = {id: 'A', format: 'baz', value: 1}; // FAILS as expected - format 'baz' is not allowed for an Entry with id: 'A'
const e3: Entry<'B'> = {id: 'B', format: 'baz', value: 4}; // OK
const i1: EntryInput = {id: 'A', format: 'bar'} // OK
const i2: EntryInput = {id: 'A', format: 'qux'} // FAILS as expected

相关问题