From 79443a6543fb6b7323c71d88d0403cf95cdfc0ca Mon Sep 17 00:00:00 2001 From: Josh Burgess Date: Sat, 7 Sep 2019 21:24:13 -0400 Subject: [PATCH 1/4] Rewrite compose utility & slightly adjust no arg test --- src/compose.ts | 188 ++++++++++++++++++++++++------------------- test/compose.spec.ts | 3 +- 2 files changed, 105 insertions(+), 86 deletions(-) diff --git a/src/compose.ts b/src/compose.ts index fcbfd70d7b..5df32b3e40 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -1,7 +1,16 @@ -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 +} + +/** + * A type-level utility that extracts the type of a function's input params as + * a tuple, inferring `unknown` for any input params assigned to generics + */ +type Params = F extends (...args: infer A) => unknown ? A : never /** * Composes single-argument functions from right to left. The rightmost @@ -13,90 +22,99 @@ 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 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))) + // `Params` is equal to `unknown[]` in the below type signature, + // but, `Params`, arguably, more clearly conveys intent + return fns.reduce((a, b) => (...args: Params) => 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) }) From ea1c69beb7aff04858c93fea0caf72cda7d7cef3 Mon Sep 17 00:00:00 2001 From: Josh Burgess Date: Sun, 8 Sep 2019 00:42:36 -0400 Subject: [PATCH 2/4] Remove Params helper in favor of the TS built-in Parameters --- src/compose.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compose.ts b/src/compose.ts index 5df32b3e40..739698d5e5 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -10,7 +10,7 @@ interface Fn extends Function { * A type-level utility that extracts the type of a function's input params as * a tuple, inferring `unknown` for any input params assigned to generics */ -type Params = F extends (...args: infer A) => unknown ? A : never +// type Params = F extends (...args: infer A) => unknown ? A : never /** * Composes single-argument functions from right to left. The rightmost @@ -114,7 +114,7 @@ export default function compose(...fns: Fns): Fn { return fns[0] } - // `Params` is equal to `unknown[]` in the below type signature, - // but, `Params`, arguably, more clearly conveys intent - return fns.reduce((a, b) => (...args: Params) => 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))) } From 58159084cf745ea554e0a9c2cb1466d8e86035e2 Mon Sep 17 00:00:00 2001 From: Josh Burgess Date: Sun, 8 Sep 2019 02:08:19 -0400 Subject: [PATCH 3/4] Add any number of fns overload & update applyMiddleware --- src/applyMiddleware.ts | 2 +- src/compose.ts | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) 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 739698d5e5..0667784e15 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -4,13 +4,23 @@ */ interface Fn extends Function { (...args: A): unknown + (...args: A[]): unknown } /** - * A type-level utility that extracts the type of a function's input params as - * a tuple, inferring `unknown` for any input params assigned to generics + * A type-level utility function to */ -// type Params = F extends (...args: infer A) => unknown ? A : never +type Tail = Fns extends ( + a: A, + ...rest: infer Rest +) => unknown + ? Rest + : never + +/** + * A type-level utility function to + */ +type Last = Fns[Tail['length']] /** * Composes single-argument functions from right to left. The rightmost @@ -102,6 +112,10 @@ export default function compose< 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 +): (...args: Parameters>) => ReturnType // generic base case type signature and function body implementation export default function compose(...fns: Fns): Fn { const len = fns.length From 89c11f37cf596619eac3a72cdf2ab5666a27785d Mon Sep 17 00:00:00 2001 From: Josh Burgess Date: Sun, 8 Sep 2019 03:08:32 -0400 Subject: [PATCH 4/4] Update type-level utils & generic compose --- src/compose.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/compose.ts b/src/compose.ts index 0667784e15..7dff351d57 100644 --- a/src/compose.ts +++ b/src/compose.ts @@ -8,19 +8,27 @@ interface Fn extends Function { } /** - * A type-level utility function to + * A type-level utility to get the type of the tail of a tuple of functions */ -type Tail = Fns extends ( - a: A, - ...rest: infer Rest -) => unknown - ? Rest - : never +type Tail = ( + ...fns: Fns +) => unknown extends (a: A, ...rest: infer Rest) => unknown ? Rest : never /** - * A type-level utility function to + * A type-level utility to get the type of the last function from a tuple of + * functions */ -type Last = Fns[Tail['length']] +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 @@ -113,9 +121,7 @@ export default function compose< fab: (...args: A) => B[0] ): (...args: A) => F // generic type signature for any number of functions -export default function compose( - ...funcs: Fns -): (...args: Parameters>) => ReturnType +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