From e12cb8ac17ad1742a1657b27464522e55dc4a463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 26 Jul 2024 13:32:56 +0200 Subject: [PATCH 01/10] add constructable type support --- src/libs/memoize/index.ts | 23 +++++++++++++---------- src/libs/memoize/types.ts | 22 +++++++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 74caeff7c3fd..0cc80adb7317 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ -import type {Constructor} from 'type-fest'; 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 +42,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 +66,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 +81,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 +93,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}; From 8a77406201dd54843a819edfbd59e933669226de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 26 Jul 2024 13:53:13 +0200 Subject: [PATCH 02/10] fix util import types --- src/libs/memoize/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index d11a55da7b04..75971232f6a2 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,7 +16,7 @@ function getEqualityComparator(options?: ClientOptions): Options { +function mergeOptions(options?: ClientOptions): Options { if (!options) { return DEFAULT_OPTIONS; } From ed84bd0f734b630b4fd534d82bbb4751dd8a70ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 26 Jul 2024 15:42:45 +0200 Subject: [PATCH 03/10] fix TakeFirst with NonPartial --- src/libs/memoize/index.ts | 3 ++- src/types/utils/NonPartial.ts | 7 +++++++ src/types/utils/TupleOperations.ts | 9 ++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/types/utils/NonPartial.ts diff --git a/src/libs/memoize/index.ts b/src/libs/memoize/index.ts index 0cc80adb7317..e14606618953 100644 --- a/src/libs/memoize/index.ts +++ b/src/libs/memoize/index.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ +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'; @@ -42,7 +43,7 @@ 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>>( +function memoize>['length'], Key = TakeFirst, MaxArgs>>( fn: Fn, opts?: ClientOptions, ) { 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..315a4472841f 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -1,12 +1,19 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type NonPartial from './NonPartial'; // Based on: https://stackoverflow.com/questions/67605122/obtain-a-slice-of-a-typescript-parameters-tuple +/** + * Split a tuple into two parts: the first `N` elements and the rest. + */ type TupleSplit = O['length'] extends N ? [O, T] : T extends readonly [infer F, ...infer R] ? TupleSplit : [O, T]; -type TakeFirst = TupleSplit[0]; +/** + * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. + */ +type TakeFirst = TupleSplit, N>[0]; export type {TupleSplit, TakeFirst}; From 474461b1923fac1aeb89395ffdf8aafc533fdc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 13:23:55 +0200 Subject: [PATCH 04/10] make TakeFirst work with rest param --- src/types/utils/TupleOperations.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index 315a4472841f..93f54d92a852 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type {PositiveInfinity} from 'type-fest'; import type NonPartial from './NonPartial'; // Based on: https://stackoverflow.com/questions/67605122/obtain-a-slice-of-a-typescript-parameters-tuple @@ -9,11 +10,14 @@ type TupleSplit = O ? [O, T] : T extends readonly [infer F, ...infer R] ? TupleSplit + : T extends readonly [...infer RR] + ? readonly [[...O, RR], T] : [O, T]; /** * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. + * `number extends N ? PositiveInfinity : N` is a hack to accomodate for rest parameters. */ -type TakeFirst = TupleSplit, N>[0]; +type TakeFirst = TupleSplit, number extends N ? PositiveInfinity : N>[0]; export type {TupleSplit, TakeFirst}; From 53fb89060ed9df72bcb4f3d8f05bd70f43fc2c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 13:25:02 +0200 Subject: [PATCH 05/10] remove readonly adnotation --- src/libs/memoize/utils.ts | 2 +- src/types/utils/TupleOperations.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index 75971232f6a2..e782eae4bcb1 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -22,7 +22,7 @@ function mergeOptions(opti } 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) { diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index 93f54d92a852..5728e5479caf 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -6,18 +6,18 @@ import type NonPartial from './NonPartial'; /** * Split a tuple into two parts: the first `N` elements and the rest. */ -type TupleSplit = O['length'] extends N +type TupleSplit = O['length'] extends N ? [O, T] - : T extends readonly [infer F, ...infer R] - ? TupleSplit - : T extends readonly [...infer RR] - ? readonly [[...O, RR], T] + : T extends [infer F, ...infer R] + ? TupleSplit<[...R], N, [...O, F]> + : T extends [...infer RR] + ? [[...O, RR], T] : [O, T]; /** * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. * `number extends N ? PositiveInfinity : N` is a hack to accomodate for rest parameters. */ -type TakeFirst = TupleSplit, number extends N ? PositiveInfinity : N>[0]; +type TakeFirst = TupleSplit, number extends N ? PositiveInfinity : N>[0]; export type {TupleSplit, TakeFirst}; From 04f18c4cc8011ad103a1a1a5cd23f7c85a88e9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 13:28:04 +0200 Subject: [PATCH 06/10] typo --- src/types/utils/TupleOperations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index 5728e5479caf..429924609b2b 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -16,7 +16,7 @@ type TupleSplit = O['length'] extends /** * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. - * `number extends N ? PositiveInfinity : N` is a hack to accomodate for rest parameters. + * `number extends N ? PositiveInfinity : N` is a hack to accommodate for rest parameters. */ type TakeFirst = TupleSplit, number extends N ? PositiveInfinity : N>[0]; From 83c085c5c90c3b09545d5924fc1698398fb2f2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 21:18:57 +0200 Subject: [PATCH 07/10] fix TupleSplit number as length --- src/types/utils/TupleOperations.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index 429924609b2b..c7aa2faf0ef4 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type {PositiveInfinity} from 'type-fest'; import type NonPartial from './NonPartial'; // Based on: https://stackoverflow.com/questions/67605122/obtain-a-slice-of-a-typescript-parameters-tuple /** * Split a tuple into two parts: the first `N` elements and the rest. */ -type TupleSplit = O['length'] extends N +type TupleSplit = number extends N + ? [T, O] + : O['length'] extends N ? [O, T] : T extends [infer F, ...infer R] ? TupleSplit<[...R], N, [...O, F]> @@ -18,6 +19,6 @@ type TupleSplit = O['length'] extends * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. * `number extends N ? PositiveInfinity : N` is a hack to accommodate for rest parameters. */ -type TakeFirst = TupleSplit, number extends N ? PositiveInfinity : N>[0]; +type TakeFirst = TupleSplit, N>[0]; export type {TupleSplit, TakeFirst}; From 8c045897cdc7909ef988950bdcbb62572283523f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 22:23:14 +0200 Subject: [PATCH 08/10] fix rest parameters --- src/types/utils/TupleOperations.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index c7aa2faf0ef4..e217e7c72e91 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -1,24 +1,27 @@ /* 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 /** * Split a tuple into two parts: the first `N` elements and the rest. */ -type TupleSplit = number extends N - ? [T, O] - : O['length'] extends N - ? [O, T] - : T extends [infer F, ...infer R] - ? TupleSplit<[...R], N, [...O, F]> - : T extends [...infer RR] - ? [[...O, RR], T] - : [O, T]; +type TupleSplit = O['length'] extends N ? [O, T] : T extends [infer F, ...infer R] ? TupleSplit<[...R], N, [...O, F]> : [O, T]; /** * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. * `number extends N ? PositiveInfinity : N` is a hack to accommodate for rest parameters. */ -type TakeFirst = TupleSplit, N>[0]; +type TakeFirst = number extends N ? T : TupleSplit>, N>[0]; export type {TupleSplit, TakeFirst}; From cbd69bcf460336e173f6346b9efea5175e90a746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 22:29:17 +0200 Subject: [PATCH 09/10] truncateArgs cast fixes --- src/libs/memoize/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/memoize/utils.ts b/src/libs/memoize/utils.ts index e782eae4bcb1..01a9e1b3abf8 100644 --- a/src/libs/memoize/utils.ts +++ b/src/libs/memoize/utils.ts @@ -34,11 +34,11 @@ function truncateArgs } if (maxArgs === 0) { - return []; + return [] as unknown as TakeFirst; } if (maxArgs === 1) { - return [args[0]]; + return [args[0]] as unknown as TakeFirst; } if (maxArgs === 2) { From a20013484d1788169d1739b8044eb9859cd01166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 30 Jul 2024 22:30:54 +0200 Subject: [PATCH 10/10] fix TakeFirst comment --- src/types/utils/TupleOperations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types/utils/TupleOperations.ts b/src/types/utils/TupleOperations.ts index e217e7c72e91..b8c056992509 100644 --- a/src/types/utils/TupleOperations.ts +++ b/src/types/utils/TupleOperations.ts @@ -19,8 +19,7 @@ type FillFromRest = O[' type TupleSplit = O['length'] extends N ? [O, T] : T extends [infer F, ...infer R] ? TupleSplit<[...R], N, [...O, F]> : [O, T]; /** - * Get the first `N` elements of a tuple. If `N` is not provided, it defaults to the length of the tuple. - * `number extends N ? PositiveInfinity : N` is a hack to accommodate for rest parameters. + * 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];