diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts index f735843252..366f630a17 100644 --- a/src/applyMiddleware.ts +++ b/src/applyMiddleware.ts @@ -72,7 +72,7 @@ export default function applyMiddleware( dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) - dispatch = compose(...chain)(store.dispatch) + dispatch = compose(...chain)(store.dispatch as typeof dispatch) return { ...store, diff --git a/src/compose.ts b/src/compose.ts index fcbfd70d7b..7dff351d57 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -1,7 +1,34 @@ -type Func0 = () => R -type Func1 = (a1: T1) => R -type Func2 = (a1: T1, a2: T2) => R -type Func3 = (a1: T1, a2: T2, a3: T3, ...args: any[]) => R +/** + * A generic representation of a function exposing more type information than + * the built-in `Function` type, allowing extraction of its input param types + */ +interface Fn extends Function { + (...args: A): unknown + (...args: A[]): unknown +} + +/** + * A type-level utility to get the type of the tail of a tuple of functions + */ +type Tail = ( + ...fns: Fns +) => unknown extends (a: A, ...rest: infer Rest) => unknown ? Rest : never + +/** + * A type-level utility to get the type of the last function from a tuple of + * functions + */ +type Last = Fns['length'] extends 0 + ? never + : Fns[Tail['length']] + +/** + * A type-level utility to get the return type of the composition of a tuple of + * functions + */ +type Composed = ( + ...args: Parameters> +) => ReturnType /** * Composes single-argument functions from right to left. The rightmost @@ -13,90 +40,101 @@ type Func3 = (a1: T1, a2: T2, a3: T3, ...args: any[]) => R * to left. For example, `compose(f, g, h)` is identical to doing * `(...args) => f(g(h(...args)))`. */ -export default function compose(): (a: R) => R - -export default function compose(f: F): F - -/* two functions */ -export default function compose(f1: (b: A) => R, f2: Func0): Func0 -export default function compose( - f1: (b: A) => R, - f2: Func1 -): Func1 -export default function compose( - f1: (b: A) => R, - f2: Func2 -): Func2 -export default function compose( - f1: (b: A) => R, - f2: Func3 -): Func3 - -/* three functions */ -export default function compose( - f1: (b: B) => R, - f2: (a: A) => B, - f3: Func0 -): Func0 -export default function compose( - f1: (b: B) => R, - f2: (a: A) => B, - f3: Func1 -): Func1 -export default function compose( - f1: (b: B) => R, - f2: (a: A) => B, - f3: Func2 -): Func2 -export default function compose( - f1: (b: B) => R, - f2: (a: A) => B, - f3: Func3 -): Func3 - -/* four functions */ -export default function compose( - f1: (b: C) => R, - f2: (a: B) => C, - f3: (a: A) => B, - f4: Func0 -): Func0 -export default function compose( - f1: (b: C) => R, - f2: (a: B) => C, - f3: (a: A) => B, - f4: Func1 -): Func1 -export default function compose( - f1: (b: C) => R, - f2: (a: B) => C, - f3: (a: A) => B, - f4: Func2 -): Func2 -export default function compose( - f1: (b: C) => R, - f2: (a: B) => C, - f3: (a: A) => B, - f4: Func3 -): Func3 - -/* rest */ -export default function compose( - f1: (b: any) => R, - ...funcs: Function[] -): (...args: any[]) => R - -export default function compose(...funcs: Function[]): (...args: any[]) => R +// when given no args and no generic type params, infer args as tuple +export default function compose(): (...a: A) => A[0] +// allow specifying type param of args tuple +export default function compose(): (...a: A) => A[0] +// when given a single function, just return that function +export default function compose( + fab: (...args: A) => B +): (...args: A) => B +// standard case, given 2 functions +export default function compose( + fbc: (b: B) => C, + fab: (...args: A) => B +): (...args: A) => C +// standard case, given 3 functions +export default function compose( + fcd: (c: C) => D, + fbc: (b: B) => C, + fab: (...args: A) => B +): (...args: A) => D +// standard case, given 4 functions +export default function compose( + fde: (d: D) => E, + fcd: (c: C) => D, + fbc: (b: B) => C, + fab: (...args: A) => B +): (...args: A) => E +// standard case, given 5 functions +export default function compose( + fef: (e: E) => F, + fde: (d: D) => E, + fcd: (c: C) => D, + fbc: (b: B) => C, + fab: (...args: A) => B +): (...args: A) => F +// extra overloads allowing functions other than the right-most function +// to take in more than one argument, though the extra arguments go unused +// 2 multi-arg functions +export default function compose( + fbc: (...b: B) => C, + fab: (...args: A) => B[0] +): (...args: A) => C +// 3 multi-arg functions +export default function compose< + A extends unknown[], + B extends unknown[], + C extends unknown[], + D +>( + fcd: (...c: C) => D, + fbc: (...b: B) => C[0], + fab: (...args: A) => B[0] +): (...args: A) => D +// 4 multi-arg functions +export default function compose< + A extends unknown[], + B extends unknown[], + C extends unknown[], + D extends unknown[], + E +>( + fde: (...d: D) => E, + fcd: (...c: C) => D[0], + fbc: (...b: B) => C[0], + fab: (...args: A) => B[0] +): (...args: A) => E +// 5 multi-arg functions +export default function compose< + A extends unknown[], + B extends unknown[], + C extends unknown[], + D extends unknown[], + E extends unknown[], + F +>( + fef: (...e: E) => F, + fde: (...d: D) => E[0], + fcd: (...c: C) => D[0], + fbc: (...b: B) => C[0], + fab: (...args: A) => B[0] +): (...args: A) => F +// generic type signature for any number of functions +export default function compose(...funcs: Fns): Composed +// generic base case type signature and function body implementation +export default function compose(...fns: Fns): Fn { + const len = fns.length -export default function compose(...funcs: Function[]) { - if (funcs.length === 0) { - // infer the argument type so it is usable in inference down the line - return (arg: T) => arg + if (len === 0) { + return (...args: A): A[0] => args[0] } - if (funcs.length === 1) { - return funcs[0] + if (len === 1) { + return fns[0] } - return funcs.reduce((a, b) => (...args: any) => a(b(...args))) + // `Parameters` is equalavelnt to `unknown[]` in the below type, + // signature, but `Parameters` more clearly conveys intent + return fns.reduce((a, b) => (...args: Parameters) => a(b(...args))) } diff --git a/test/compose.spec.ts b/test/compose.spec.ts index d25131cdeb..a6cdef98ca 100644 --- a/test/compose.spec.ts +++ b/test/compose.spec.ts @@ -104,7 +104,8 @@ describe('Utils', () => { }) it('returns the first given argument if given no functions', () => { - expect(compose()(1, 2)).toBe(1) + expect(compose<[number, number]>()(1, 2)).toBe(1) + expect(compose()('zero', 1, 2)).toBe('zero') expect(compose()(3)).toBe(3) expect(compose()(undefined)).toBe(undefined) })