Collected · Redux · MIT
Temporary exhibition · Hall of the Commons
Thirty Signatures for Fifteen Lines
TypeScript·2015·61 lines·1563 bytes
type Func<T extends any[], R> = (...a: T) => R
/**
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for the
* resulting composite function.
*
* @param funcs The functions to compose.
* @returns A function obtained by composing the argument functions from right
* to left. For example, `compose(f, g, h)` is identical to doing
* `(...args) => f(g(h(...args)))`.
*/
export default function compose(): <R>(a: R) => R
export default function compose<F extends Function>(f: F): F
/* two functions */
export default function compose<A, T extends any[], R>(
f1: (a: A) => R,
f2: Func<T, A>
): Func<T, R>
/* three functions */
export default function compose<A, B, T extends any[], R>(
f1: (b: B) => R,
f2: (a: A) => B,
f3: Func<T, A>
): Func<T, R>
/* four functions */
export default function compose<A, B, C, T extends any[], R>(
f1: (c: C) => R,
f2: (b: B) => C,
f3: (a: A) => B,
f4: Func<T, A>
): Func<T, R>
/* rest */
export default function compose<R>(
f1: (a: any) => R,
...funcs: Function[]
): (...args: any[]) => R
export default function compose<R>(...funcs: Function[]): (...args: any[]) => R
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce(
(a, b) =>
(...args: any) =>
a(b(...args))
)
}Curator’s note
Sixty-one lines, of which the working code is the last fifteen. Everything above it is the type system being asked for something it cannot give.
What compose does is one line: take functions and nest them, so that compose(f, g, h) behaves as (...args) => f(g(h(...args))). The implementation is a single reduce, and it would be the same single reduce in any language with first-class functions. It is the plainest possible expression of an idea that has been in mathematics since long before there were computers.
Now count the declarations above it. There is one for no arguments, one for a single function, one for two, one for three, one for four, and then a catch-all — because TypeScript cannot express "a list of functions where each one's output type is the next one's input type" for a list of unknown length. That is a statement about the type system rather than about Redux. The constraint is real, other type systems can express it, and the ones that can pay for it elsewhere.
So the overloads are not clumsiness. They are somebody deciding that four composed functions is as far as anyone reasonably goes, writing the exact types for those cases, and falling back to any past it. The final signature returns (...args: any[]) => R, which is the file admitting where the guarantee stops.
It hangs beside createStore from the same project because the pair says something neither says alone. One is a hundred lines of runtime doing real work. The other is fifteen lines of runtime carrying forty-five lines of apology to the compiler, and both shipped in the same library on the same day.