function memoize(functionToMemoize, { cacheStorage = {} } = {}) {
  /** @type {CacheStrategyItem|undefined} */
  let currentCacheStrategyItem;
  function memoizedFn() {
    // eslint-disable-next-line prefer-rest-params
    const args = [...arguments];
    const shouldSerialize = args.some(isObject);

    const cachableArgs = shouldSerialize ? args.map(createCachableArg) : args;
    // Allow disabling of cache for testing purposes
    // @ts-expect-error
    if (shouldCache && cachableArgs in cacheStorage) {
      updateCacheStrategyItem(/** @type {CacheStrategyItem} */ (currentCacheStrategyItem));

      // @ts-expect-error
      return cacheStorage[cachableArgs];
    }

    if (!currentCacheStrategyItem) {
      updateCacheStrategyItemsList();
      currentCacheStrategyItem = addCacheStrategyItem(memoizedFn);
    }

    // @ts-expect-error
    const outcome = functionToMemoize.apply(this, args);
    // @ts-expect-error
    // eslint-disable-next-line no-param-reassign
    cacheStorage[cachableArgs] = outcome;
    return outcome;
  }
  memoizedFn.clearCache = () => {
    // eslint-disable-next-line no-param-reassign
    cacheStorage = {};
    currentCacheStrategyItem = undefined;
  };

  return /** @type {* & T & {clearCache:() => void}} */ (memoizedFn);
}
@template T
@type {<T extends Function>(functionToMemoize:T, opts?:{ cacheStorage?:object; }) => T & {clearCache:() => void}}