合并多个 TypeScript 枚举的通用函数

Generic function to merge multiple TypeScript enums

我正在尝试编写一个通用函数来合并多个枚举。希望这个函数能完成与以下相同的功能:

enum Mammals {
  Humans = 'Humans',
  Bats = 'Bats',
  Dolphins = 'Dolphins',
}

enum Reptiles {
  Snakes = 'Snakes',
  Alligators = 'Alligators',
  Lizards = 'Lizards',
}

const Animals = {
 ...Mammals,
 ...Reptiles,
}

type Animals = Mammals | Reptiles;

第一次尝试:

export const mergeEnums = <T extends any[]>(...enums: T): T[number] => {
  return {
    ...enums,
  };
};

// Results in Animals: typeof Mammals | typeof Reptiles
const Animals = mergeEnums(Mammals, Reptiles);

不幸的是,联合类型不太正确。 TypeScript 不允许访问键。类型错误示例:Property 'Snakes' does not exist on type 'typeof Mammals | typeof Reptiles'.

第二次尝试:

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I,
) => void
  ? I
  : never;

export const mergeEnums = <T extends any[]>(
  ...enums: T
): UnionToIntersection<T[number]> => {
  return {
    ...enums,
  } as UnionToIntersection<T[number]>;
};

// Results in Animals: typeof Mammals & typeof Reptiles
const Animals = mergeEnums(Mammals, Reptiles);

这确实允许密钥访问,但是当相同的密钥存在于多个枚举中时,会导致 return 类型的 never,这在我的使用中是一种可能性。

是否有可能实现与 Animals: Mammals | Reptiles 功能相同的解决方案?

假设您想尝试在类型系统中模拟展开运算符在运行时使用冲突属性所做的事情,您可以通过调整我对 的回答来实现。开头是这样的:

type OptionalPropertyNames<T> =
  { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];

type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

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

// Type of { ...L, ...R }
type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  & Pick<L, Exclude<keyof L, keyof R>>
  // Properties in R with types that exclude undefined
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  // Properties in R, with types that include undefined, that don't exist in L
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  // Properties in R, with types that include undefined, that exist in L
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;

可选属性非常令人头疼,因为如果我有一个像 {a: string} 这样的对象并将类型 {a?: number} 的对象散布到其中,生成的对象将是一个 {a: string | number}。另一个问题的所有注意事项都适用于此:有很多边缘情况。坦率地说,您从 Object.assign() 或通用传播获得的默认交叉点并不比上面的混乱差多少,而且要简单得多。如果您确定您的用例需要,我只建议使用 Spread<L, R> 代替 L & R


无论如何,继续讨论可变元组部分。 TypeScript 4.1 将引入递归条件类型(在 microsoft/TypeScript#40002 中实现),因此您可以将 Merge<T> 表示为对 Spread<L, R>:

的递归操作
type Merge<T extends readonly any[]> = 
  T extends readonly [infer H, ...infer R] ? Spread<H, Merge<R>> : {}

export const mergeEnums = <T extends any[]>(
  ...enums: T
) => {
  return {
    ...enums,
  } as any as Merge<T>;
};

这应该会如您所愿:

enum Mammals {
  Humans = 'Humans',
  Bats = 'Bats',
  Dolphins = 'Dolphins',
}

enum Reptiles {
  Snakes = 'Snakes',
  Alligators = 'Alligators',
  Lizards = 'Lizards',
}

const Animals = mergeEnums(Mammals, Reptiles);
type Animals = typeof Animals[keyof typeof Animals];

const Animals = mergeEnums(Mammals, Reptiles);
/* const Animals: {
    readonly Humans: Mammals.Humans;
    readonly Bats: Mammals.Bats;
    readonly Dolphins: Mammals.Dolphins;
    readonly Snakes: Reptiles.Snakes;
    readonly Alligators: Reptiles.Alligators;
    readonly Lizards: Reptiles.Lizards;
} */
type Animals = typeof Animals[keyof typeof Animals];
// type Animals = Mammals | Reptiles

如果你允许碰撞,你会得到我希望的结果:

enum Reptiles {
  Humans = 'They Have Discovered That We Are Alien Invaders; KILL THEM ALL',
  Snakes = 'Snakes',
  Alligators = 'Alligators',
  Lizards = 'Lizards',
}

const Animals = mergeEnums(Mammals, Reptiles);
/* const Animals: {
    readonly Bats: Mammals.Bats;
    readonly Dolphins: Mammals.Dolphins;
    readonly Humans: Reptiles.Humans;
    readonly Snakes: Reptiles.Snakes;
    readonly Alligators: Reptiles.Alligators;
    readonly Lizards: Reptiles.Lizards;
} */

type Animals = typeof Animals[keyof typeof Animals];
// type Animals = Mammals.Bats | Mammals.Dolphins | Reptiles

Playground link to code