Typing compose function in TypeScript
yossarian
Posted on June 22, 2021
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
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;
}
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;
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
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)
}
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
Posted on June 22, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.