Last active
May 8, 2022 20:38
-
-
Save jfet97/a9938bc1602371a42ba9f437e77b9355 to your computer and use it in GitHub Desktop.
merge union into object mantaining keys union and modifiers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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