From 02fcd3bbb33d8e4920175624450525436202737c Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 12 May 2024 22:41:46 +1200 Subject: [PATCH] fix: type when merging optional properties of a record --- project-dictionary.txt | 2 + src/types/defaults.ts | 230 ++++++++++++++++++++++++++------- src/types/utils.ts | 219 +++++++++++++++---------------- tests/deepmerge-into.test-d.ts | 14 ++ tests/deepmerge.test-d.ts | 11 ++ 5 files changed, 320 insertions(+), 156 deletions(-) diff --git a/project-dictionary.txt b/project-dictionary.txt index 287685d8..dd8f3ec0 100644 --- a/project-dictionary.txt +++ b/project-dictionary.txt @@ -19,6 +19,7 @@ kinded klass knip litecoin +metas monero noreply octocat @@ -29,5 +30,6 @@ Rebecca sonarjs Stevens thud +tuplify waldo xyzzy diff --git a/src/types/defaults.ts b/src/types/defaults.ts index 1c5b138c..55ed9e8f 100644 --- a/src/types/defaults.ts +++ b/src/types/defaults.ts @@ -6,13 +6,14 @@ import { } from "./merging"; import { type FilterOutNever, + type FlattenTuples, type FlatternAlias, - type OptionalKeysOf, - type RequiredKeysOf, + type TransposeTuple, + type TupleToIntersection, + type TuplifyUnion, type UnionMapKeys, type UnionMapValues, type UnionSetValues, - type ValueOfKey, } from "./utils"; /** @@ -46,6 +47,38 @@ export type DeepMergeMergeFunctionsDefaultURIs = Readonly<{ DeepMergeOthersURI: DeepMergeLeafURI; }>; +type RecordEntries> = TuplifyUnion< + { + [K in keyof T]: [K, T[K]]; + }[keyof T] +>; + +type RecordMeta = Record; + +type RecordPropertyMeta< + Key extends PropertyKey = PropertyKey, + Value = unknown, + Optional extends boolean = boolean, +> = { + key: Key; + value: Value; + optional: Optional; +}; + +type RecordsToRecordMeta< + Ts extends ReadonlyArray>, +> = { + [I in keyof Ts]: RecordToRecordMeta; +}; + +type RecordToRecordMeta> = { + [K in keyof T]-?: { + key: K; + value: Required[K]; + optional: {} extends Pick ? true : false; + }; +}; + /** * Deep merge records. */ @@ -54,66 +87,169 @@ export type DeepMergeRecordsDefaultHKT< MF extends DeepMergeMergeFunctionsURIs, M, > = - Ts extends Readonly>]> - ? FlatternAlias> - : {}; + Ts extends ReadonlyArray> + ? FlatternAlias< + DeepMergeRecordMetaDefaultHKTProps, MF, M> + > + : never; /** * Deep merge record props. */ -type DeepMergeRecordsDefaultHKTInternalProps< - Ts extends readonly [unknown, ...ReadonlyArray], +type DeepMergeRecordMetaDefaultHKTProps< + RecordMetas extends ReadonlyArray, MF extends DeepMergeMergeFunctionsURIs, M, -> = { - [K in OptionalKeysOf]?: DeepMergeHKT< - DeepMergeRecordsDefaultHKTInternalPropValue, - MF, - M - >; -} & { - [K in RequiredKeysOf]: DeepMergeHKT< - DeepMergeRecordsDefaultHKTInternalPropValue, - MF, - M +> = CreateRecordFromMeta, MF, M>; + +type MergeRecordMeta> = + GroupValuesByKey< + FlattenTuples< + TransposeTuple<{ + [I in keyof RecordMetas]: TransposeTuple>; + }> + > >; -}; -/** - * Get the value of the property. - */ -type DeepMergeRecordsDefaultHKTInternalPropValue< - Ts extends readonly [unknown, ...ReadonlyArray], - K extends PropertyKey, +type GroupValuesByKey = Ts extends readonly [ + infer Keys extends ReadonlyArray, + infer Values, +] + ? { + [I in keyof Keys]: DeepMergeRecordPropertyMetaDefaultHKTGetPossible< + Keys[I], + FilterOutNever<{ + [J in keyof Values]: Values[J] extends { + key: Keys[I]; + } + ? Values[J] + : never; + }> + >; + } + : never; + +type CreateRecordFromMeta = + Ts extends ReadonlyArray + ? TupleToIntersection<{ + [I in keyof Ts]: Ts[I] extends { + key: infer Key extends PropertyKey; + values: infer Values extends ReadonlyArray; + optional: infer O extends boolean; + } + ? CreateRecordForKeyFromMeta + : never; + }> + : never; + +type CreateRecordForKeyFromMeta< + Key extends PropertyKey, + Values extends ReadonlyArray, + Optional extends boolean, + MF extends DeepMergeMergeFunctionsURIs, M, -> = FilterOutNever< - DeepMergeRecordsDefaultHKTInternalPropValueHelper ->; +> = Optional extends true + ? { + [k in Key]+?: DeepMergeHKT; + } + : { + [k in Key]-?: DeepMergeHKT; + }; /** - * Tail-recursive helper type for DeepMergeRecordsDefaultHKTInternalPropValue. + * Get the possible types of a property. */ -type DeepMergeRecordsDefaultHKTInternalPropValueHelper< - Ts extends readonly [unknown, ...ReadonlyArray], - K extends PropertyKey, - M, - Acc extends ReadonlyArray, +type DeepMergeRecordPropertyMetaDefaultHKTGetPossible< + Key extends PropertyKey, + Ts, > = Ts extends readonly [ - infer Head extends Readonly>, + RecordPropertyMeta, + ...ReadonlyArray, +] + ? DeepMergeRecordPropertyMetaDefaultHKTGetPossibleHelper< + Ts, + { key: Key; values: []; optional: never } + > + : never; + +/** + * Tail-recursive helper type for DeepMergeRecordPropertyMetaDefaultHKTGetPossible. + */ +type DeepMergeRecordPropertyMetaDefaultHKTGetPossibleHelper< + Ts extends readonly [ + RecordPropertyMeta, + ...ReadonlyArray, + ], + Acc extends { + key: PropertyKey; + values: ReadonlyArray; + optional: boolean; + }, +> = Ts extends [ ...infer Rest, + { + key: infer K extends PropertyKey; + value: infer V; + optional: infer O extends boolean; + }, ] - ? Rest extends readonly [unknown, ...ReadonlyArray] - ? DeepMergeRecordsDefaultHKTInternalPropValueHelper< - Rest, - K, - M, - [...Acc, ValueOfKey] - > - : [...Acc, ValueOfKey] + ? Acc["optional"] extends true + ? Acc extends { values: [infer Head, ...infer AccRest] } + ? Rest extends readonly [ + RecordPropertyMeta, + ...ReadonlyArray, + ] + ? DeepMergeRecordPropertyMetaDefaultHKTGetPossibleHelper< + Rest, + { + key: K; + values: [V | Head, ...AccRest]; + optional: O; + } + > + : { + key: K; + values: [V | Head, ...AccRest]; + optional: O; + } + : Rest extends readonly [ + RecordPropertyMeta, + ...ReadonlyArray, + ] + ? DeepMergeRecordPropertyMetaDefaultHKTGetPossibleHelper< + Rest, + { + key: K; + values: [V, ...Acc["values"]]; + optional: O; + } + > + : { + key: K; + values: [V, ...Acc["values"]]; + optional: O; + } + : Rest extends readonly [ + RecordPropertyMeta, + ...ReadonlyArray, + ] + ? DeepMergeRecordPropertyMetaDefaultHKTGetPossibleHelper< + Rest, + { + key: K; + values: [V, ...Acc["values"]]; + optional: O; + } + > + : { + key: K; + values: [V, ...Acc["values"]]; + optional: O; + } : never; /** - * Deep merge 2 arrays. + * Deep merge arrays. */ export type DeepMergeArraysDefaultHKT< Ts extends ReadonlyArray, @@ -142,14 +278,14 @@ type DeepMergeArraysDefaultHKTHelper< : never; /** - * Deep merge 2 sets. + * Deep merge sets. */ export type DeepMergeSetsDefaultHKT> = Set< UnionSetValues >; /** - * Deep merge 2 maps. + * Deep merge maps. */ export type DeepMergeMapsDefaultHKT> = Map< UnionMapKeys, diff --git a/src/types/utils.ts b/src/types/utils.ts index e927409e..e2f85ff6 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -2,19 +2,29 @@ * Flatten a complex type such as a union or intersection of objects into a * single object. */ -export type FlatternAlias = T extends {} ? FlatternRecord : T; +export type FlatternAlias = T extends {} ? FlattenRecord : T; -type FlatternRecord = { +type FlattenRecord = { [K in keyof T]: T[K]; } & {}; /** - * Get the value of the given key in the given object. + * Flatten a collection of tuples of tuples into a collection of tuples. */ -export type ValueOfKey< - T extends Record, - K extends PropertyKey, -> = K extends keyof T ? T[K] : never; +export type FlattenTuples = { + [I in keyof T]: FlattenTuple; +}; + +/** + * Flatten a tuple of tuples into a single tuple. + */ +export type FlattenTuple = T extends readonly [] + ? [] + : T extends readonly [infer T0] + ? [...FlattenTuple] + : T extends readonly [infer T0, ...infer Ts] + ? [...FlattenTuple, ...FlattenTuple] + : [T]; /** * Safely test whether or not the first given types extends the second. @@ -28,6 +38,25 @@ export type Is = [T1] extends [T2] ? true : false; */ export type IsNever = Is; +/** + * And operator for types. + */ +export type And = T1 extends false + ? false + : T2; + +/** + * Or operator for types. + */ +export type Or = T1 extends true + ? true + : T2; + +/** + * Not operator for types. + */ +export type Not = T extends true ? false : true; + /** * Returns whether or not all the given types are never. */ @@ -130,25 +159,6 @@ export type EveryIsMap> = : false : false; -/** - * And operator for types. - */ -export type And = T1 extends false - ? false - : T2; - -/** - * Or operator for types. - */ -export type Or = T1 extends true - ? true - : T2; - -/** - * Not operator for types. - */ -export type Not = T extends true ? false : true; - /** * Union of the sets' values' types */ @@ -209,92 +219,11 @@ type UnionMapValuesHelper< : never : Acc; -/** - * Get all the keys of the given records. - */ -export type KeysOf> = KeysOfHelper; - -/** - * Tail-recursive helper type for KeysOf. - */ -type KeysOfHelper< - Ts extends ReadonlyArray, - Acc, -> = Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Record - ? Rest extends ReadonlyArray - ? KeysOfHelper - : Acc | keyof Head - : never - : Acc; - -/** - * Get the keys of the type what match a certain criteria. - */ -type KeysOfType = { - [K in keyof T]: T[K] extends U ? K : never; -}[keyof T]; - -/** - * Get the required keys of the type. - */ -type RequiredKeys = Exclude< - KeysOfType>, - undefined ->; - -/** - * Get all the required keys on the types in the tuple. - */ -export type RequiredKeysOf< - Ts extends readonly [unknown, ...ReadonlyArray], -> = RequiredKeysOfHelper; - -/** - * Tail-recursive helper type for RequiredKeysOf. - */ -type RequiredKeysOfHelper< - Ts extends readonly [unknown, ...ReadonlyArray], - Acc, -> = Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Record - ? Rest extends readonly [unknown, ...ReadonlyArray] - ? RequiredKeysOfHelper> - : Acc | RequiredKeys - : never - : Acc; - -/** - * Get the optional keys of the type. - */ -type OptionalKeys = Exclude>; - -/** - * Get all the optional keys on the types in the tuple. - */ -export type OptionalKeysOf< - Ts extends readonly [unknown, ...ReadonlyArray], -> = OptionalKeysOfHelper; - -/** - * Tail-recursive helper type for OptionalKeysOf. - */ -type OptionalKeysOfHelper< - Ts extends readonly [unknown, ...ReadonlyArray], - Acc, -> = Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Record - ? Rest extends readonly [unknown, ...ReadonlyArray] - ? OptionalKeysOfHelper> - : Acc | OptionalKeys - : never - : Acc; - /** * Filter out nevers from a tuple. */ -export type FilterOutNever> = - FilterOutNeverHelper; +export type FilterOutNever = + T extends ReadonlyArray ? FilterOutNeverHelper : never; /** * Tail-recursive helper type for FilterOutNever. @@ -318,3 +247,75 @@ export type IsTuple> = T extends readonly [] : T extends readonly [unknown, ...ReadonlyArray] ? true : false; + +/** + * Perfrom a transpose operation on a 2D tuple. + */ +export type TransposeTuple = T extends readonly [ + ...(readonly [...unknown[]]), +] + ? T extends readonly [] + ? [] + : T extends readonly [infer X extends ReadonlyArray] + ? TransposeTupleSimpleCase + : T extends readonly [ + infer X extends ReadonlyArray, + ...infer XS extends ReadonlyArray>, + ] + ? PrependCol> + : T + : never; + +type PrependCol< + T extends ReadonlyArray, + S extends ReadonlyArray>, +> = T extends readonly [] + ? S extends readonly [] + ? [] + : never + : T extends readonly [infer X, ...infer XS] + ? S extends readonly [ + readonly [...infer Y], + ...infer YS extends ReadonlyArray>, + ] + ? [[X, ...Y], ...PrependCol] + : never + : never; + +type TransposeTupleSimpleCase = + T extends readonly [] + ? [] + : T extends readonly [infer X, ...infer XS] + ? [[X], ...TransposeTupleSimpleCase] + : never; + +/** + * Convert a tuple to an intersection of each of its types. + */ +export type TupleToIntersection> = + { + [K in keyof T]: (x: T[K]) => void; + } extends Record void> + ? I + : never; + +/** + * Convert a union to a tuple. + * + * Warning: The order of the elements is non-deterministic. + * Warning 2: The union maybe me modified by the TypeScript engine before convertion. + * Warning 3: This implementation relies on a hack/limitation in TypeScript. + */ +export type TuplifyUnion> = + IsNever extends true ? [] : [...TuplifyUnion>, L]; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never; + +type LastOf = + UnionToIntersection T : never> extends () => infer R + ? R + : never; diff --git a/tests/deepmerge-into.test-d.ts b/tests/deepmerge-into.test-d.ts index c8baac1c..62f5b1ab 100644 --- a/tests/deepmerge-into.test-d.ts +++ b/tests/deepmerge-into.test-d.ts @@ -81,3 +81,17 @@ expectAssignable<{ bar: "123"; quux: "456"; }>(test6); + +const d: { waldo: boolean; fred?: number } = { waldo: false }; + +const test7 = { ...a }; +deepmergeInto(test7, d); +expectAssignable<{ + foo: string; + baz: { + quux: string[]; + }; + garply: number; + waldo: boolean; + fred?: number; +}>(test7); diff --git a/tests/deepmerge.test-d.ts b/tests/deepmerge.test-d.ts index 0fcaad12..f0a62cdd 100644 --- a/tests/deepmerge.test-d.ts +++ b/tests/deepmerge.test-d.ts @@ -228,3 +228,14 @@ const test16 = deepmerge(first, second, third, fourth); expectType<{ first: boolean; second: boolean; third: number; fourth: string }>( test16, ); + +const n: { a: true; b: string } = { a: true, b: "n" }; +const o: { a: false; b?: number } = { a: false }; + +const test17 = deepmerge(n, o); +expectType<{ a: false; b: string | number }>(test17); + +const p: { a: true; b?: string } = { a: true, b: "n" }; + +const test18 = deepmerge(o, p); +expectType<{ a: true; b?: string | number }>(test18);