Assuming you want to try to emulate in the type system what the spread operator does at runtime with colliding properties, you could do it by adapting my answer to this question. It starts out like this:
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>
>;
Optional properties are quite the headache, since if I have an object like {a: string}
and spread an object of type {a?: number}
into it, the resulting object will be an {a: string | number}
. All the caveats from the other question apply here: there are very many edge cases. The default intersection you get from Object.assign()
or a generic spread is, frankly, not much worse than the mess above and is a lot simpler. I'd only recommend going with Spread<L, R>
in place of L & R
if you are sure your use case warrants it.
Anyway, moving on to the variadic tuple part. TypeScript 4.1 will introduce recursive conditional types (as implemented in microsoft/TypeScript#40002) so you can represent your Merge<T>
as a recursive operation on 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>;
};
This should work as you expect:
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
and if you allow for collision, you get what I hope is your desired outcome:
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