Skip to content

Instantly share code, notes, and snippets.

@jfet97
Last active May 8, 2022 20:38
Show Gist options
  • Save jfet97/a9938bc1602371a42ba9f437e77b9355 to your computer and use it in GitHub Desktop.
Save jfet97/a9938bc1602371a42ba9f437e77b9355 to your computer and use it in GitHub Desktop.
merge union into object mantaining keys union and modifiers
type AllKeys<T> = T extends unknown ? keyof T : never;
type Lookup<T, K extends PropertyKey> = T extends any ? T[K & keyof T] : never;
type MergeAsUnion<T> = { [K in AllKeys<T>]: Lookup<T, K> };
type IfEquals<T, U, Y = unknown, N = never> = (<V>() => V extends T
? 1
: 2) extends <V>() => V extends U ? 1 : 2
? Y
: N;
export type Extends<A, B, Y = unknown, N = never> = [A] extends [never]
? N
: A extends B
? Y
: N;
type Resolve<T> = T extends Function ? T : { [K in keyof T]: T[K] };
type SyntheticFilterObjectByKey<O, K, T = any> = {
[KEY in keyof O as KEY extends K ? KEY : never]: T;
};
// true if at least one element of the original union has KEY_OF_MERGED_OBJECT as a readonly-optional property
// OR one element has KEY_OF_MERGED_OBJECT as a readonly property and another element has KEY_OF_MERGED_OBJECT as an optional property
type WasReadonlyAndOptional<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> = Extends<
WasReadonlyAndOptionalCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
KEY_OF_MERGED_OBJECT,
Extends<
WasReadonlyCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
Extends<
WasOptionalCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
KEY_OF_MERGED_OBJECT
>
>
>;
type WasReadonlyAndOptionalCore<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> =
// are readonly-optional modifiers the same as at least one ORIGINAL_UNION_EL[KEY_OF_MERGED_OBJECT]?
ORIGINAL_UNION extends infer ORIGINAL_UNION_EL
? // second argument is a homomorphic mapped type that preserves modifiers of the ORIGINAL_UNION_EL
IfEquals<
{ readonly [K in KEY_OF_MERGED_OBJECT]?: any },
SyntheticFilterObjectByKey<ORIGINAL_UNION_EL, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT
>
: never;
// true if at least one element of the original union has KEY_OF_MERGED_OBJECT as a readonly property
// readonly but never readonly-optional
type WasReadonly<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> = Extends<
WasReadonlyAndOptional<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
never,
WasReadonlyCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>
>;
type WasReadonlyCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT extends PropertyKey> =
// check samenes of modifiers with at least one ORIGINAL_UNION_EL[KEY_OF_MERGED_OBJECT]?
ORIGINAL_UNION extends infer ORIGINAL_UNION_EL
? IfEquals<
{ readonly [K in KEY_OF_MERGED_OBJECT]: any },
SyntheticFilterObjectByKey<ORIGINAL_UNION_EL, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT
>
: never;
// true if at least one element of the original union has KEY_OF_MERGED_OBJECT as an optional property
// optional but never readonly-optional
type WasOptional<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> = Extends<
WasReadonlyAndOptional<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
never,
WasOptionalCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>
>;
type WasOptionalCore<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> = ORIGINAL_UNION extends infer ORIGINAL_UNION_EL
? IfEquals<
{ [K in KEY_OF_MERGED_OBJECT]?: any },
SyntheticFilterObjectByKey<ORIGINAL_UNION_EL, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT
>
: never;
// never readonly-optional, never readonly, never optional
type WasPlain<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> = Extends<
WasReadonlyAndOptional<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
never,
Extends<
WasReadonly<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
never,
Extends<
WasOptional<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT,
never,
WasPlainCore<ORIGINAL_UNION, KEY_OF_MERGED_OBJECT>
>
>
>;
type WasPlainCore<
ORIGINAL_UNION,
KEY_OF_MERGED_OBJECT extends PropertyKey
> = ORIGINAL_UNION extends infer ORIGINAL_UNION_EL
? IfEquals<
{ [K in KEY_OF_MERGED_OBJECT]: any },
SyntheticFilterObjectByKey<ORIGINAL_UNION_EL, KEY_OF_MERGED_OBJECT>,
KEY_OF_MERGED_OBJECT
>
: never;
type RestoreModifiers<ORIGINAL_UNION, MERGED_UNION> = Resolve<
{
readonly // restore readonly-optional keys
[K in keyof MERGED_UNION as WasReadonlyAndOptional<
ORIGINAL_UNION,
K
>]?: MERGED_UNION[K];
} & {
// restore optional keys
[K in keyof MERGED_UNION as WasOptional<
ORIGINAL_UNION,
K
>]?: MERGED_UNION[K];
} & {
readonly // restore readonly keys
[K in keyof MERGED_UNION as WasReadonly<
ORIGINAL_UNION,
K
>]: MERGED_UNION[K];
} & {
// restore other keys
[K in keyof MERGED_UNION as WasPlain<ORIGINAL_UNION, K>]: MERGED_UNION[K];
}
>;
//
type union =
| { readonly prop1: number; readonly prop2: string; readonly prop4?: number }
| { prop2?: boolean; prop3: string[]; prop5: 5 }
| { prop3?: [boolean]; prop4: "test"; prop5: 55 };
type merged = MergeAsUnion<union>;
// if at least one instance of a key is readonly-optional, it will be readonly-optional in the resulting type
// else if at least one instance of a key is readonly and another instance of the same key is optional, it will be readonly-optional in the resulting type
// else if at least one instance of a key is readonly, it will be readonly in the resulting type
// else if at least one instance of a key is optional, it will be optional in the resulting type
// else no modifier are applied to the key
type mergedWithModifier = RestoreModifiers<union, merged>;
/*
{
readonly prop2?: string | boolean | undefined;
readonly prop4?: number | "test" | undefined;
prop3?: string[] | [boolean] | undefined;
readonly prop1: number;
prop5: 5 | 55;
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment