diff --git a/libs/common/src/types/options.spec.ts b/libs/common/src/types/options.spec.ts index 412673683f3..bb2ad9d9670 100644 --- a/libs/common/src/types/options.spec.ts +++ b/libs/common/src/types/options.spec.ts @@ -9,6 +9,10 @@ type ExampleOptions = { readonly d?: number; readonly e: string; }; + readonly f: { + readonly h?: number; + readonly i: string; + }; }; const EXAMPLE_DEFAULTS = Object.freeze({ @@ -27,12 +31,16 @@ describe("mergeOptions", () => { const options: ExampleOptions = { required_func: () => {}, b: "test", + f: { + i: "example", + }, }; const merged = mergeOptions(options, EXAMPLE_DEFAULTS); // can access properties expect(merged.a).toBe(42); + expect(merged.c.d).toBe(1); expect(merged).toEqual({ a: 0, diff --git a/libs/common/src/types/options.ts b/libs/common/src/types/options.ts index 4cdccf8adf0..04623a87923 100644 --- a/libs/common/src/types/options.ts +++ b/libs/common/src/types/options.ts @@ -1,151 +1,36 @@ -/* eslint-disable @typescript-eslint/no-empty-object-type -- used in type-fest's code*/ -import { RequiredKeysOf, Simplify, Primitive } from "type-fest"; +import { Simplify, Primitive, ConditionalKeys } from "type-fest"; type Function = (...args: any[]) => any; -/** FIXME: this is pulled from type-fest-v4. remove when we update package */ -export type ConditionalSimplifyDeep< - Type, - ExcludeType = never, - IncludeType = unknown, -> = Type extends ExcludeType - ? Type - : Type extends IncludeType - ? { [TypeKey in keyof Type]: ConditionalSimplifyDeep } - : Type; -export type BuiltIns = Primitive | void | Date | RegExp; -export type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown); -export type SimplifyDeep = ConditionalSimplifyDeep< - Type, - ExcludeType | NonRecursiveType | Set | Map, - object ->; +/** FIXME: taken from type-fest-v4. remove once we upgrade */ +export type RequiredKeysOf = BaseType extends unknown // For distributing `BaseType` + ? Exclude> + : never; // Should never happen +export type OptionalKeysOf = BaseType extends unknown // For distributing `BaseType` + ? keyof { + [Key in keyof BaseType as BaseType extends Record ? never : Key]: never; + } & + keyof BaseType // Intersect with `keyof BaseType` to ensure result of `OptionalKeysOf` is always assignable to `keyof BaseType` + : never; // Should never happen -type SimplifyDeepExcludeArray = SimplifyDeep; -export type UnknownArray = readonly unknown[]; -export type UnknownRecord = Record; -export type UnknownArrayOrTuple = readonly [...unknown[]]; -export type OmitIndexSignature = { - [KeyType in keyof ObjectType as {} extends Record - ? never - : KeyType]: ObjectType[KeyType]; -}; -export type PickIndexSignature = { - [KeyType in keyof ObjectType as {} extends Record - ? KeyType - : never]: ObjectType[KeyType]; -}; +/** END FIXME: taken from type-fest-v4. remove once we upgrade */ -type MergeDeepRecordProperty = undefined extends Source - ? - | MergeDeepOrReturn, Exclude> - | undefined - : MergeDeepOrReturn; -type RequiredFilter = undefined extends Type[Key] - ? Type[Key] extends undefined - ? Key - : never - : Key; - -// Returns `never` if the key is required otherwise return the key type. -type OptionalFilter = undefined extends Type[Key] - ? Type[Key] extends undefined - ? never - : Key - : never; - -export type EnforceOptional = Simplify< +// Merges options with defaults, deeply preferring default types over options types. +// FIXME: It's likely the type-fests `MergeDeep` will be a better fit for this, but that is in v4+, +// marked experimental, _and_ caused flaky build failures when testing. +export type OptionsWithDefaultsDeep = Simplify< { - [Key in keyof ObjectType as RequiredFilter]: ObjectType[Key]; + [K in keyof Required & keyof Defaults]: Defaults[K] extends Primitive | Function + ? Defaults[K] + : Defaults[K] extends object + ? // note: exclude undefined here so that if the type is a union with undefined, we can still get the keys correctly + OptionsWithDefaultsDeep, Defaults[K]> + : never; // should only ever be primitive, function, or object } & { - [Key in keyof ObjectType as OptionalFilter]?: Exclude< - ObjectType[Key], - undefined - >; + [K in Exclude, keyof Defaults>]: Options[K]; } >; -type MergeDeepOrReturn = SimplifyDeepExcludeArray< - [undefined] extends [Destination | Source] - ? DefaultType - : Destination extends UnknownRecord - ? Source extends UnknownRecord - ? MergeDeepRecord - : DefaultType - : Destination extends UnknownArrayOrTuple - ? Source extends UnknownArrayOrTuple - ? MergeDeepArrayOrTuple - : DefaultType - : DefaultType ->; - -type MergeDeepArrayOrTuple< - Destination extends UnknownArrayOrTuple, - Source extends UnknownArrayOrTuple, -> = Array[number] | Exclude[number]>; - -type MergeDeepRecord< - Destination extends UnknownRecord, - Source extends UnknownRecord, -> = DoMergeDeepRecord, OmitIndexSignature> & - Merge, PickIndexSignature>; - -type DoMergeDeepRecord = - // Case in rule 1: The destination contains the key but the source doesn't. - { - [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key]; - } & { - // Case in rule 2: The source contains the key but the destination doesn't. - [Key in keyof Source as Key extends keyof Destination ? never : Key]: Source[Key]; - } & { - // Case in rule 3: Both the source and the destination contain the key. - [Key in keyof Source as Key extends keyof Destination ? Key : never]: MergeDeepRecordProperty< - Destination[Key], - Source[Key] - >; - }; -type SimpleMerge = { - [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key]; -} & Source; - -export type Merge = Simplify< - SimpleMerge, PickIndexSignature> & - SimpleMerge, OmitIndexSignature> ->; -export type IfNever = - IsNever extends true ? TypeIfNever : TypeIfNotNever; -export type IsNever = [T] extends [never] ? true : false; - -type MergeDeep = SimplifyDeepExcludeArray< - [undefined] extends [Destination | Source] - ? never - : Destination extends UnknownRecord - ? Source extends UnknownRecord - ? MergeDeepRecord - : never - : Destination extends UnknownArrayOrTuple - ? Source extends UnknownArrayOrTuple - ? MergeDeepArrayOrTuple - : never - : never ->; -export type ConditionalKeys = { - // Map through all the keys of the given base type. - [Key in keyof Base]-?: Base[Key] extends Condition // Pick only keys with types extending the given `Condition` type. - ? // Retain this key - // If the value for the key extends never, only include it if `Condition` also extends never - IfNever, Key> - : // Discard this key since the condition fails. - never; - // Convert the produced object into a union type of the keys which passed the conditional test. -}[keyof Base]; - -/** END FIXME: this is pulled from type-fest-v4. remove when we update package */ - -export type OptionsWithDefaultsDeep = - // FIXME: replace with MergeDeep when type-fest is updated to v4+ - MergeDeep; - type DefaultsForDeep = Simplify< Omit< Required<{ @@ -171,10 +56,10 @@ type RequiredMethodKeysOf = RequiredKeysOf< export function mergeOptions< Options extends object, - const Defaults extends DefaultsForDeep, + // const Defaults extends DefaultsForDeep, >( options: Options, - defaults: Defaults, + defaults: DefaultsForDeep, //Defaults, ): OptionsWithDefaultsDeep> { const result = { ...options } as any; for (const key in defaults) {