Write a higher-order function memoize(fn) that returns a new function with the same behaviour as fn, except results are cached by argument. Repeat calls with the same argument skip fn entirely and return the cached value. This is the classic memoization pattern — a single-argument wrapper backed by a Map, useful for pure functions that are expensive to recompute.
// Returns a wrapped version of `fn` that caches by its single argument.
// Subsequent calls with an argument already in the cache return the stored
// result and DO NOT invoke `fn` again.
function memoize<A, R>(fn: (arg: A) => R): (arg: A) => R;
let calls = 0;
const square = memoize((n) => {
calls++;
return n * n;
});
square(5); // 25 (calls === 1)
square(5); // 25 (calls === 1 — cache hit, fn not invoked)
square(6); // 36 (calls === 2 — distinct arg, fn invoked once more)
square(5); // 25 (calls === 2 — still cached)
// Different argument types each get their own cache entry.
const id = memoize((x) => x);
id(1); // 1
id('1'); // '1' (a separate slot — string '1' !== number 1)
id(null); // null
id(undefined); // undefined
id({ a: 1 }); // { a: 1 } (cached by reference, not by shape)
memoize wraps unary functions. Don't try to handle (a, b) here — that's a multi-arg variant and is out of scope for this question.Map, keyed on the argument as-is. Use new Map() (not a plain object). The argument is the key directly — no JSON.stringify, no coercion. That means lookup is reference equality for objects/arrays, value equality for primitives.fn on a cache hit. Even if the cached value is undefined, a second call with the same argument must return the stored undefined without calling fn. Use cache.has(key) to test presence, not cache.get(key) (which can't distinguish "missing" from "stored undefined").fn returned the first time for that argument — no cloning, no freezing.this. Tests call the memoized function as a plain function. You can ignore Function.prototype.call/apply binding for this exercise.