Skip to content

Instantly share code, notes, and snippets.

@alexitaylor
Last active October 18, 2022 17:58
Show Gist options
  • Save alexitaylor/c297a548db516166f7d77fb07914c79d to your computer and use it in GitHub Desktop.
Save alexitaylor/c297a548db516166f7d77fb07914c79d to your computer and use it in GitHub Desktop.
TypeScript Memoize Util Function. Caches the result of async functions. Function's arguments are converted into hash key. Plus tests.
/**
* createCacheKeyFromArgs
*
* @desc Creates a cache key from fn's arguments.
*
* @param {any[]} args Arguments from fn being memoized.
* @return {string} Returns a cache key.
*/
export const createCacheKeyFromArgs = (args: any[]) =>
args.reduce((cacheKey, arg) => (cacheKey += `_${typeof arg === 'object' ? JSON.stringify(args) : `${arg}`}_`), '');
/**
* memoize
*
* @desc Creates a function that memoizes the result of fn.
* The arguments of the fn are used to create a cache key.
*
* @param {Function} fn The function to have its output memoized.
* @return {Function} Returns the new memoized function.
*/
const memoize: (...args: any[]) => any = function(fn) {
const cache = new Map();
return function() {
const cacheKey = createCacheKeyFromArgs(Array.from(arguments));
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const asyncFn = fn.apply(this, arguments);
cache.set(cacheKey, asyncFn);
return asyncFn;
};
};
export default memoize;
/**
* ===========================
* ===========================
* Jest Tests
* ===========================
* ===========================
*/
const expect = require('expect');
const mocks = {
add: jest.fn((x, y) => x + y)
};
const addMemoize = memoize(mocks.add);
afterEach(() => {
mocks.add.mockClear();
});
const asyncAddMemoize = memoize(function(x, y) {
let promise = new Promise(resolve => {
setTimeout(() => {
resolve(mocks.add(x, y));
}, 500);
});
return promise;
});
describe('memoize', () => {
it('should create a cache key from arguments', () => {
// Arrange
const args = [1, 3];
// Act
const cacheKey = createCacheKeyFromArgs(args);
// Assert
expect(cacheKey).toEqual('_1__3_');
});
it('should create a cache key from an object argument', () => {
// Arrange
const args = [{ a: 1 }];
// Act
const cacheKey = createCacheKeyFromArgs(args);
// Assert
expect(cacheKey).toEqual('_[{"a":1}]_');
});
it('should return expected result of add fn', () => {
// Arrange / Act
const result = addMemoize(1, 2);
// Assert
expect(result).toEqual(3);
});
it('should memoize the result of add fn and only call it once', () => {
expect.assertions(4);
// Arrange
const addSpy = jest.spyOn(mocks, 'add');
// Act
const resultOne = addMemoize(2, 2);
const resultTwo = addMemoize(2, 2);
const resultThree = addMemoize(2, 2);
// Assert
expect(resultOne).toEqual(4);
expect(resultTwo).toEqual(4);
expect(resultThree).toEqual(4);
expect(addSpy).toBeCalledTimes(1);
});
it('should return expected result of async add fn', async () => {
// Arrange / Act
const result = await asyncAddMemoize(2, 3);
// Assert
expect(result).toEqual(5);
});
it('should memoize the result of add async fn and only call it once', async () => {
expect.assertions(4);
// Arrange
const addSpy = jest.spyOn(mocks, 'add');
// Act
const [resultOne, resultTwo, resultThree] = await Promise.all([
asyncAddMemoize(3, 3),
asyncAddMemoize(3, 3),
asyncAddMemoize(3, 3)
]);
// Assert
expect(resultOne).toEqual(6);
expect(resultTwo).toEqual(6);
expect(resultThree).toEqual(6);
expect(addSpy).toBeCalledTimes(1);
});
});
@Properko
Copy link

Properko commented Mar 7, 2022

super useful, thanks, here's a version that preserves types :)

const memoize = <ARGS extends unknown[], RET>(fn: (...args: ARGS) => RET) => {
  const cache: Record<string, RET> = {};

  return (...args: ARGS) => {
    const cacheKey = createCacheKeyFromArgs(args);

    if (cache[cacheKey]) {
      return cache[cacheKey];
    }

    const asyncFn = fn.call(undefined, ...args);
    cache[cacheKey] = asyncFn;
    return asyncFn;
  };
};

// ====
const sum = async (a: number, b: number) => a + b;
// const memoizedSum: (a: number, b: number) => Promise<number>
const memoizedSum = memoize(sum);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment