Card 1 / 10
Wrap a function so it runs at most once: the first call runs it and remembers the result, and every later call returns that remembered result without calling the function again.
| call | result | runs fn? |
|---|---|---|
const add = once((a: number, b: number) => a + b) |
โ | โ |
add(2, 3) |
5 |
yes |
add(10, 20) |
5 |
no โ the cached 5 comes back and the new arguments are ignored |
Reading the signature:
<A extends unknown[], R> โ A stands for the whole argument list (an array type) and R for the return type, so the wrapper advertises exactly fn's shape.(...args: A) is a rest parameter: it collects however many arguments the caller passed into one array. fn(...args) spreads that array back out as separate arguments, so any signature forwards through unchanged.Declare a called flag and a result variable before the return, and invoke fn only while called is still false.
function once<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R {
// Return a wrapper that runs fn at most once and caches its result for later calls.
let called = false;
let result: R;
return (...args: A): R => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}Card 2 / 10
Wrap a function so repeated calls with the same arguments return a cached result instead of recomputing. Key the cache by the stringified arguments.
| call | result | times fn actually ran |
|---|---|---|
const square = memoize((n: number) => n * n) |
โ | 0 |
square(4) |
16 |
1 |
square(4) |
16 |
1 โ served from the cache |
square(5) |
25 |
2 |
Reading the signature:
<A extends unknown[], R> โ A is the argument-list type and R the return type, so the wrapper keeps fn's exact shape.(...args: A) is a rest parameter: it collects the caller's arguments into an array, and fn(...args) spreads them back out as separate arguments.Map is the cache: cache.has(key) asks whether an entry exists, cache.set(key, value) stores one, and cache.get(key) reads it back. It is declared beside the returned function, so the closure keeps it alive across calls.JSON.stringify(args) turns the whole argument array into one string, giving a single key that works for any argument shape โ [1, 2] becomes "[1,2]".cache.get(key) is typed R | undefined, because in general a key may be missing. The trailing ! is a non-null assertion: it tells TypeScript you have just guaranteed the entry is there.Build a Map<string, R> outside the returned function. Inside, compute JSON.stringify(args) as the key, fill the entry when it is missing, then return it.
function memoize<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R {
// Return a wrapper that caches results keyed by the stringified arguments.
const cache = new Map<string, R>();
return (...args: A): R => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key)!;
};
}Card 3 / 10
Return a function that yields 1, then 2, then 3, and so on each time it is called. Separate counters must not share state.
| call | result | why |
|---|---|---|
const next = makeCounter() |
โ | a fresh counter, nothing emitted yet |
next() |
1 |
first call |
next() |
2 |
the same counter carries on |
makeCounter()() |
1 |
a different counter, counting from its own zero |
Reading the signature:
() => number means "a function that takes no arguments and returns a number" โ makeCounter hands back a function, not a number.makeCounter, and the function you return keeps it alive after makeCounter has already finished. Each call to makeCounter creates a new variable, which is exactly why two counters are independent.++n is pre-increment: it adds one and evaluates to the new value, so the first call yields 1. n++ would evaluate to the old value and start at 0.Declare let n = 0 before the return, then return () => ++n.
function makeCounter(): () => number {
// Return a function that returns 1, then 2, then 3, ... on successive calls.
let n = 0;
return () => ++n;
}Card 4 / 10
Combine two functions into one that applies the second first, then the first: compose(f, g) returns x => f(g(x)).
f |
g |
x |
compose(f, g)(x) |
|---|---|---|---|
n => n + 1 |
n => n * 2 |
5 |
11 โ double to 10, then increment |
n => n * 2 |
n => n + 1 |
5 |
12 โ increment to 6, then double |
s => s.toUpperCase() |
s => s.trim() |
" hi " |
"HI" |
Reading the signature:
(a: A) => B is a function type: "takes an A, returns a B". The parameter name inside it is documentation only.<A, B, C> chains the three types: g turns an A into a B, f turns that B into a C, and the composed function turns an A straight into a C. B is the handoff point, so g's return type has to be f's parameter type โ a mismatched pair is rejected before you ever run it.Pipe card is the same idea written left-to-right.Return a function of x that feeds x to g and hands g's output to f.
function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B): (a: A) => C {
// Return a function that applies g first, then f: x => f(g(x)).
return (x: A) => f(g(x));
}Card 5 / 10
Pre-bind the leading arguments of a function, returning a new function that takes the remaining ones: partial(add3, 1, 2)(3) is add3(1, 2, 3).
fn |
fixed |
call on the result | result |
|---|---|---|---|
add3 = (a, b, c) => a + b + c |
1, 2 |
(3) |
6 |
add3 |
10 |
(20, 30) |
60 |
greet = (greeting, name) => `${greeting}, ${name}!` |
"Hello" |
("World") |
"Hello, World!" |
Reading the signature:
...fixed: any[] is a rest parameter on partial itself: every argument after fn is collected into the fixed array, so the caller can pre-bind as many as they like.fn(...fixed, ...rest) spreads two arrays into a single argument list โ the pre-bound ones first, then the ones supplied later. Spreading both in one call is what makes the split invisible to fn.fixed lives in the closure: it is captured when partial runs and stays available on every later call of the returned function.any[] on purpose. Expressing "the parameters of fn minus the ones already supplied" needs variadic tuple types; this card is about the mechanic, so only the return type R is tracked precisely.Return a function taking ...rest, and call fn with both arrays spread in order: fn(...fixed, ...rest).
function partial<R>(fn: (...args: any[]) => R, ...fixed: any[]): (...rest: any[]) => R {
// Return a function with the leading args pre-bound; remaining args are supplied later.
return (...rest: any[]) => fn(...fixed, ...rest);
}Card 6 / 10
Turn a two-argument function into a chain of one-argument functions: curry2(fn)(a)(b) equals fn(a, b). The intermediate function can be reused with different second arguments.
| call | result |
|---|---|
const add = (a: number, b: number) => a + b |
โ |
curry2(add)(2)(3) |
5 |
const add10 = curry2(add)(10) |
a function still waiting for b |
add10(5) |
15 |
Reading the signature:
(a: A) => (b: B) => R reads right-associatively: "takes an A and returns a function that takes a B and returns an R". Two arrows means two separate calls, which is why the result is used as curry2(fn)(a)(b) rather than curry2(fn)(a, b).a is captured by the closure. The inner function is created while a is in scope and keeps it alive, so add10 still knows its 10 on every later call.Return an arrow taking a whose body is another arrow taking b that calls fn(a, b).
function curry2<A, B, R>(fn: (a: A, b: B) => R): (a: A) => (b: B) => R {
// Turn a two-argument function into a chain: fn(a, b) becomes curry2(fn)(a)(b).
return (a: A) => (b: B) => fn(a, b);
}Card 7 / 10
Combine any number of single-argument functions into one that runs them left-to-right on an input. With no functions, the result is the identity function.
fns |
x |
pipe(...fns)(x) |
|---|---|---|
n => n + 1, n => n * 2 |
5 |
12 โ 5 โ 6 โ 12 |
n => n * 2, n => n + 1 |
5 |
11 โ 5 โ 10 โ 11 |
| (none) | 7 |
7 โ nothing to apply, so the input passes straight through |
Reading the signature:
...fns: ((x: T) => T)[] is a rest parameter typed as an array of function types. (x: T) => T means "takes a T and returns a T" โ every stage has the same type, which is what lets them chain in any order and any number.T to T, the empty case has an obvious answer: with nothing to apply, the input is already the result.array.reduce(callback, initial) folds an array into a single value: it starts from initial and calls callback(accumulator, element) for each element, left to right, feeding each result in as the next accumulator. Here the accumulator is the value being transformed and each element is a function to apply โ so the fold is the pipeline, and reduce's left-to-right order is exactly pipe's order.compose over many functions is the same fold run with reduceRight, which walks the array from the end instead. This card uses reduce.Thread the value through with a fold: fns.reduce((acc, fn) => fn(acc), x).
function pipe<T>(...fns: ((x: T) => T)[]): (x: T) => T {
// Return a function that runs the given functions left-to-right on an input value.
return (x: T) => fns.reduce((acc, fn) => fn(acc), x);
}Card 8 / 10
Wrap a predicate so it returns the opposite boolean: negate(isEven) is an isOdd test.
pred |
call on the result | result |
|---|---|---|
n => n % 2 === 0 |
(3) |
true โ 3 is not even |
n => n % 2 === 0 |
(4) |
false |
() => true |
() |
false |
Reading the signature:
<A extends unknown[]> makes A stand for the whole argument list as an array type, so negate accepts a predicate of any arity โ one argument, several, or none at all.(...args: A) is a rest parameter: it collects whatever the caller passes into an array, and pred(...args) spreads it back out as separate arguments. That pair is how a wrapper forwards arguments it knows nothing about.pred returns a boolean, and so does the wrapper.Return a function that forwards its ...args to pred and puts ! in front of the result.
function negate<A extends unknown[]>(pred: (...args: A) => boolean): (...args: A) => boolean {
// Return a predicate that returns the opposite boolean of pred.
return (...args: A) => !pred(...args);
}Card 9 / 10
Wrap a function so it only runs from the nth call onward. The first n - 1 calls return undefined; the nth call and every call after it invoke the function.
With const go = after(3, () => "done"):
| call | result | calls so far |
|---|---|---|
go() |
undefined |
1 |
go() |
undefined |
2 |
go() |
"done" |
3 โ the count reached n, so fn runs |
go() |
"done" |
4 โ and every call from here on |
Reading the signature:
(...args: A) is a rest parameter: it collects the caller's arguments into an array, and fn(...args) spreads them back out, so the wrapper forwards any signature unchanged.R | undefined is a union โ "either fn's result or undefined". It has to admit both, because the early calls genuinely produce nothing, and saying so forces the caller to handle that case.after gets its own.Keep a counter in the closure, increment it on every call, and invoke fn only once the counter has reached n.
function after<A extends unknown[], R>(
n: number,
fn: (...args: A) => R,
): (...args: A) => R | undefined {
// Return a function that only invokes fn once it has been called n or more times.
// Earlier calls return undefined.
let count = 0;
return (...args: A): R | undefined => {
count++;
if (count >= n) return fn(...args);
return undefined;
};
}Card 10 / 10
Wrap a two-argument function so its first two arguments are swapped: flip(fn)(b, a) calls fn(a, b).
| call | result |
|---|---|
const sub = (a: number, b: number) => a - b |
โ |
sub(10, 3) |
7 |
flip(sub)(10, 3) |
-7 โ the wrapper runs sub(3, 10) |
flip((a: string, b: string) => a + b)("a", "b") |
"ba" |
Reading the signature:
(a: A, b: B) => R is a function type: "takes an A and a B, returns an R". The parameter names inside it are documentation; what the compiler enforces is the position and type of each parameter.fn: (a: A, b: B) => R and the returned (b: B, a: A) => R differ only in the order of those types. That is the lesson here: the flipped function's first argument must now be a B, so the swap is visible in the type and not just in the body.Return a function whose parameters are (b, a), and call fn(a, b) inside it.
function flip<A, B, R>(fn: (a: A, b: B) => R): (b: B, a: A) => R {
// Return a function that calls fn with its first two arguments swapped.
return (b: B, a: A) => fn(a, b);
}