diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 74caeff7c3fd..e14606618953 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ -import type {Constructor} from 'type-fest'; +import type NonPartial from '@src/types/utils/NonPartial'; import type {TakeFirst} from '@src/types/utils/TupleOperations'; import ArrayCache from './cache/ArrayCache'; import {MemoizeStats} from './stats'; -import type {ClientOptions, MemoizedFn, MemoizeFnPredicate, Stats} from './types'; +import type {Callable, ClientOptions, Constructable, IsomorphicFn, IsomorphicParameters, IsomorphicReturnType, MemoizedFn, Stats} from './types'; import {getEqualityComparator, mergeOptions, truncateArgs} from './utils'; /** @@ -43,14 +43,18 @@ class Memoize { * @param opts - Options for the memoization layer, for more details see `ClientOptions` type. * @returns Memoized function with a cache API attached to it. */ -function memoize['length'], Key = TakeFirst, MaxArgs>>(fn: Fn, opts?: ClientOptions) { +function memoize>['length'], Key = TakeFirst, MaxArgs>>( + fn: Fn, + opts?: ClientOptions, +) { const options = mergeOptions(opts); - const cache = ArrayCache>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); + const cache = ArrayCache>({maxSize: options.maxSize, keyComparator: getEqualityComparator(options)}); const stats = new MemoizeStats(options.monitor || Memoize.isMonitoringEnabled); - const memoized = function memoized(...args: Parameters): ReturnType { + const memoized = function memoized(...args: IsomorphicParameters): IsomorphicReturnType { + // Detect if memoized function was called with `new` keyword. If so we need to call the original function as constructor. const constructable = !!new.target; const truncatedArgs = truncateArgs(args, options.maxArgs); @@ -63,9 +67,9 @@ function memoize { const fnTimeStart = performance.now(); - // If the function is constructable, we need to call it with the `new` keyword - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const result = constructable ? new (fn as unknown as Constructor, Parameters>)(...args) : fn(...args); + + const result = (constructable ? new (fn as Constructable)(...args) : (fn as Callable)(...args)) as IsomorphicReturnType; + statsEntry.trackTime('fnTime', fnTimeStart); statsEntry.track('didHit', false); @@ -78,7 +82,7 @@ function memoize; /** * Cache API attached to the memoized function. Currently there is an issue with typing cache keys, but the functionality works as expected. @@ -90,7 +94,7 @@ function memoize; + return memoized; } export default memoize; diff --git a/src/libs/memoize/types.ts b/src/libs/memoize/types.ts index c32bcd2bfd9b..80a6b4c55507 100644 --- a/src/libs/memoize/types.ts +++ b/src/libs/memoize/types.ts @@ -3,7 +3,15 @@ import type {Cache} from './cache/types'; import type {MemoizeStats} from './stats'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type MemoizeFnPredicate = (...args: any[]) => any; +type Callable = (...args: any[]) => any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Constructable = new (...args: any[]) => any; + +type IsomorphicFn = Callable | Constructable; + +type IsomorphicParameters = Fn extends Callable ? Parameters : Fn extends Constructable ? ConstructorParameters : never; + +type IsomorphicReturnType = Fn extends Callable ? ReturnType : Fn extends Constructable ? Fn : never; type KeyComparator = (k1: Key, k2: Key) => boolean; @@ -11,7 +19,7 @@ type InternalOptions = { cache: 'array'; }; -type Options = { +type Options = { maxSize: number; equality: 'deep' | 'shallow' | KeyComparator; monitor: boolean; @@ -24,15 +32,15 @@ type Options = { * @param truncatedArgs - Tuple of arguments passed to the memoized function (truncated to `maxArgs`). Does not work with constructable (see description). * @returns - Key to use for caching */ - transformKey?: (truncatedArgs: TakeFirst, MaxArgs>) => Key; + transformKey?: (truncatedArgs: TakeFirst, MaxArgs>) => Key; } & InternalOptions; -type ClientOptions = Partial, keyof InternalOptions>>; +type ClientOptions = Partial, keyof InternalOptions>>; type Stats = Pick; -type MemoizedFn = Fn & { - cache: Cache>; +type MemoizedFn = Fn & { + cache: Cache>; } & Stats; -export type {Options, ClientOptions, MemoizeFnPredicate, Stats, KeyComparator, MemoizedFn}; +export type {Options, ClientOptions, IsomorphicFn, IsomorphicParameters, IsomorphicReturnType, Stats, KeyComparator, MemoizedFn, Callable, Constructable}; diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index d11a55da7b04..01a9e1b3abf8 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -1,9 +1,9 @@ import {deepEqual, shallowEqual} from 'fast-equals'; import type {TakeFirst} from '@src/types/utils/TupleOperations'; import DEFAULT_OPTIONS from './const'; -import type {ClientOptions, KeyComparator, MemoizeFnPredicate, Options} from './types'; +import type {ClientOptions, IsomorphicFn, KeyComparator, Options} from './types'; -function getEqualityComparator(opts: Options): KeyComparator { +function getEqualityComparator(opts: Options): KeyComparator { // Use the custom equality comparator if it is provided if (typeof opts.equality === 'function') { return opts.equality; @@ -16,13 +16,13 @@ function getEqualityComparator(options?: ClientOptions): Options { +function mergeOptions(options?: ClientOptions): Options { if (!options) { return DEFAULT_OPTIONS; } return {...DEFAULT_OPTIONS, ...options}; } -function truncateArgs(args: T, maxArgs?: MaxArgs): TakeFirst { +function truncateArgs(args: T, maxArgs?: MaxArgs): TakeFirst { // Hot paths are declared explicitly to avoid the overhead of the slice method if (maxArgs === undefined) { @@ -34,11 +34,11 @@ function truncateArgs; } if (maxArgs === 1) { - return [args[0]]; + return [args[0]] as unknown as TakeFirst; } if (maxArgs === 2) { diff --git a/src/types/utils/NonPartial.ts b/src/types/utils/NonPartial.ts new file mode 100644 index 000000000000..aea7c22b60ad --- /dev/null +++ b/src/types/utils/NonPartial.ts @@ -0,0 +1,7 @@ +/** + * Utility type to make all properties of an object non-optional with `undefined` as a possible value. It fixes the issue with `Required` utility type, which makes all properties required. + * See https://github.com/microsoft/TypeScript/issues/31025 for more details. + */ +type NonPartial> = {[K in keyof RT]: K extends keyof T ? (undefined extends T[K] ? T[K] | undefined : T[K]) : never}; + +export default NonPartial; diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index 1fc755c7e603..b8c056992509 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -1,12 +1,26 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type NonPartial from './NonPartial'; + +/** + * Fill a tuple with `N` elements from the rest of the tuple. + */ +type FillFromRest = O['length'] extends N + ? O + : T extends [infer Head, ...infer Tail] + ? FillFromRest + : T extends [...infer Rest] + ? FillFromRest + : O; // Based on: https://stackoverflow.com/questions/67605122/obtain-a-slice-of-a-typescript-parameters-tuple -type TupleSplit = O['length'] extends N - ? [O, T] - : T extends readonly [infer F, ...infer R] - ? TupleSplit - : [O, T]; +/** + * Split a tuple into two parts: the first `N` elements and the rest. + */ +type TupleSplit = O['length'] extends N ? [O, T] : T extends [infer F, ...infer R] ? TupleSplit<[...R], N, [...O, F]> : [O, T]; -type TakeFirst = TupleSplit[0]; +/** + * Get the first `N` elements of a tuple. If `N` is not provided, it returns the whole tuple. + */ +type TakeFirst = number extends N ? T : TupleSplit>, N>[0]; export type {TupleSplit, TakeFirst};