type-fest
type-fest copied to clipboard
MergeUnion
Hi,
I would like to have merge types of two objects but have a union of "each" attribute individually (not whole object).
Example
interface A {
id: number;
serial: number;
}
interface B {
id: string;
serial: string;
}
type C = Partial<A> | Partial<B>;
// I want to have combined type below:
const c: C = { id:3, serial: "j" }; // ERROR: Both must be number or both must be string
I couldn't find the necessary types in type-fest. Is it possible?
If not, there is a solution. The idea is not mine, I found it here, could you please consider adding this:
interface A {
id: number;
serial: number;
}
interface B {
id: string;
serial: string;
}
type Compute<T> = { [K in keyof T]: T[K] } | never;
type AllKeys<T> = T extends any ? keyof T : never;
type MergeUnion<T, Keys extends keyof T = keyof T> = Compute<
{ [K in Keys]: T[Keys] } & { [K in AllKeys<T>]?: T extends any ? (K extends keyof T ? T[K] : never) : never }
>;
type C = Partial<A> | Partial<B>;
type D = MergeUnion<A | B>;
const c: C = { id:3, serial: "j" }; // ERROR
const d: D = { id:3, serial: "j" }; // OK
Upvote & Fund
- We're using Polar.sh so you can upvote and help fund this issue.
- The funding will be given to active contributors.
- Thank you in advance for helping prioritize & fund our backlog.
The util above needs to be distributive over Keys or it breaks merging multiple keys:
type T = MergeUnion<{ a: string, b: number } | { a: string, b?: number }
// Actual: { a: string | undefined, b?: number | undefined }
// Expect: { a: string, b?: number | undefined }
type T = MergeUnion<{ a: string, b: number } | { a: string, b: undefined }>
// Actual: { a: string | undefined, b: number | undefined }
// Expect: { a: string, b: number | undefined }
Updated to make it distributive, to use the existing utilities in type-fest, and to make the public interface of MergeUnion not include keys:
import type { Simplify, KeysOfUnion } from 'type-fest';
type _MergeUnionKnownKeys<
BaseType extends object,
Keys extends keyof BaseType = keyof BaseType,
> = {
[K in Keys]: Keys extends K ? BaseType[Keys] : never;
};
export type MergeUnion<BaseType extends object> = Simplify<
_MergeUnionKnownKeys<BaseType> & {
[K in KeysOfUnion<BaseType>]?: BaseType extends object
? K extends keyof BaseType
? BaseType[K]
: never
: never;
}
>;
Test Cases
// for testing results
function assertType<Expected>(): <Actual extends Expected>(
...args: IsEqual<Actual, Expected> extends true
? [actual: Actual]
: [actual: Actual] & [expected: Expected] & "Types did not match"
) => Actual {
return (actual) => actual
}
declare var x: any;
// Case: Present in both, same type
assertType<{ prop: string }>()(x as MergeUnion<{ prop: string } | { prop: string }>)
// Case: Present in both, different types
assertType<{ prop: string | number }>()(x as MergeUnion<{ prop: string } | { prop: number }>)
// Case: Present in both, both optional
assertType<{ prop?: string }>()(x as MergeUnion<{ prop?: string } | { prop?: string }>)
// Case: Only present in one
assertType<{ prop?: string }>()(x as MergeUnion<{ prop: string } | {}>)
// Case: Present in both, one is never
assertType<{ prop: string }>()(x as MergeUnion<{ prop: string } | { prop: never }>)
// Case: Present in both, one is unknown
assertType<{ prop: unknown }>()(x as MergeUnion<{ prop: string } | { prop: unknown }>)
// Q: Should this be `{ prop?: string }`
// Case: Present in both, one is undefined
assertType<{ prop: string | undefined }>()(x as MergeUnion<{ prop: string } | { prop: undefined }>)
// Case: Present in both, one is record
assertType<Simplify<{ prop: string | number } & { [k: string]: number | undefined }>>()(x as MergeUnion<{ prop: string } | Record<string, number>>)
// Case: Multiple keys, present in both, same type
assertType<{ prop: string, other: number }>()(x as MergeUnion<{ prop: string, other: number } | { prop: string, other: number }>)
// Case: Multiple keys, present in both, different types
assertType<{ prop: string | symbol, other: number | boolean }>()(x as MergeUnion<{ prop: string, other: number } | { prop: symbol, other: boolean }>)
// Case: Multiple keys, both optional
assertType<{ prop?: string, other?: number }>()(x as MergeUnion<{ prop?: string, other?: number } | { prop?: string, other?: number }>)
// Case: Multiple keys, optional only in one key in one union member
assertType<{ prop: string, other?: number }>()(x as MergeUnion<{ prop: string, other: number } | { prop: string, other?: number }>)
// Case: Multiple keys, maybe undefined only in one key in one union member
assertType<{ prop: string, other: number | undefined }>()(x as MergeUnion<{ prop: string, other: number } | { prop: string, other: undefined }>)
// Case: Multiple keys, only present in one
assertType<{ prop?: string, other?: number }>()(x as MergeUnion<{ prop: string, other: number } | {}>)