DEV Community

yossarian
yossarian

Posted on • Edited on • Originally published at catchts.com

Typing compose function in TypeScript

Let's write some crazy, unreadable and unmaintainable typings for compose function. Maybe you will learn smth new.

Let's define some base types and utils.

type Fn = (a: any) => any type Head<T extends any[]> = T extends [infer H, ...infer _] ? H : never; type Last<T extends any[]> = T extends [infer _] ? never : T extends [...infer _, infer Tl] ? Tl : never; type Foo = typeof foo type Bar = typeof bar type Baz = typeof baz 
Enter fullscreen mode Exit fullscreen mode

Our main goal is to make compose without any arguments length restriction.

For example, take a look on lodash compose typings:

 interface LodashFlowRight { <A extends any[], R1, R2, R3, R4, R5, R6, R7>(f7: (a: R6) => R7, f6: (a: R5) => R6, f5: (a: R4) => R5, f4: (a: R3) => R4, f3: (a: R2) => R3, f2: (a: R1) => R2, f1: (...args: A) => R1): (...args: A) => R7; <A extends any[], R1, R2, R3, R4, R5, R6>(f6: (a: R5) => R6, f5: (a: R4) => R5, f4: (a: R3) => R4, f3: (a: R2) => R3, f2: (a: R1) => R2, f1: (...args: A) => R1): (...args: A) => R6; <A extends any[], R1, R2, R3, R4, R5>(f5: (a: R4) => R5, f4: (a: R3) => R4, f3: (a: R2) => R3, f2: (a: R1) => R2, f1: (...args: A) => R1): (...args: A) => R5; <A extends any[], R1, R2, R3, R4>(f4: (a: R3) => R4, f3: (a: R2) => R3, f2: (a: R1) => R2, f1: (...args: A) => R1): (...args: A) => R4; <A extends any[], R1, R2, R3>(f3: (a: R2) => R3, f2: (a: R1) => R2, f1: (...args: A) => R1): (...args: A) => R3; <A extends any[], R1, R2>(f2: (a: R1) => R2, f1: (...args: A) => R1): (...args: A) => R2; (...func: Array<lodash.Many<(...args: any[]) => any>>): (...args: any[]) => any; } 
Enter fullscreen mode Exit fullscreen mode

There is a limit for arguments.

Let's try to write function without any limits, at least explicit limits. Please keep in mind TS has his own recursion limits, so we have to live with that

I will start with validation logic:

type Allowed< T extends Fn[], Cache extends Fn[] = [] > = T extends [] ? Cache : T extends [infer Lst] ? Lst extends Fn ? Allowed<[], [...Cache, Lst]> : never : T extends [infer Fst, ...infer Lst] ? Fst extends Fn ? Lst extends Fn[] ? Head<Lst> extends Fn ? Head<Parameters<Fst>> extends ReturnType<Head<Lst>> ? Allowed<Lst, [...Cache, Fst]> : never : never : never : never : never; 
Enter fullscreen mode Exit fullscreen mode

Above type iterates through every function in the array and checks if argument of current function is assignable to return type of next function Head<Parameters<Fst>> extends ReturnType<Head<Lst>>

Next, we can define simple helpers:

 type LastParameterOf<T extends Fn[]> = Last<T> extends Fn ? Head<Parameters<Last<T>>> : never type Return<T extends Fn[]> = Head<T> extends Fn ? ReturnType<Head<T>> : never 
Enter fullscreen mode Exit fullscreen mode

Finally, our compose function:

 function compose<T extends Fn, Fns extends T[], Allow extends { 0: [never], 1: [LastParameterOf<Fns>] }[Allowed<Fns> extends never ? 0 : 1]> (...args: [...Fns]): (...data: Allow) => Return<Fns> function compose< T extends Fn, Fns extends T[], Allow extends unknown[] >(...args: [...Fns]) { return (...data: Allow) => args.reduceRight((acc, elem) => elem(acc), data) } 
Enter fullscreen mode Exit fullscreen mode

As you might have noticed, I have defined only one overload, this is considered a bad practice. We should always define at least two. Sorry for that.

And full example for copy/paste:

 type Foo = typeof foo type Bar = typeof bar type Baz = typeof baz type Fn = (a: any) => any type Head<T extends any[]> = T extends [infer H, ...infer _] ? H : never; type Last<T extends any[]> = T extends [infer _] ? never : T extends [...infer _, infer Tl] ? Tl : never; type Allowed< T extends Fn[], Cache extends Fn[] = [] > = T extends [] ? Cache : T extends [infer Lst] ? Lst extends Fn ? Allowed<[], [...Cache, Lst]> : never : T extends [infer Fst, ...infer Lst] ? Fst extends Fn ? Lst extends Fn[] ? Head<Lst> extends Fn ? Head<Parameters<Fst>> extends ReturnType<Head<Lst>> ? Allowed<Lst, [...Cache, Fst]> : never : never : never : never : never; type LastParameterOf<T extends Fn[]> = Last<T> extends Fn ? Head<Parameters<Last<T>>> : never type Return<T extends Fn[]> = Head<T> extends Fn ? ReturnType<Head<T>> : never function compose<T extends Fn, Fns extends T[], Allow extends { 0: [never], 1: [LastParameterOf<Fns>] }[Allowed<Fns> extends never ? 0 : 1]> (...args: [...Fns]): (...data: Allow) => Return<Fns> function compose< T extends Fn, Fns extends T[], Allow extends unknown[] >(...args: [...Fns]) { return (...data: Allow) => args.reduceRight((acc, elem) => elem(acc), data) } const foo = (arg: 1 | 2) => [1, 2, 3] const bar = (arg: string) => arg.length > 10 ? 1 : 2 const baz = (arg: number[]) => 'hello' const check = compose(foo, bar, baz)([1, 2, 3]) // [number] const check2 = compose(bar, foo)(1) // expected error 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)