A type safe and fully variadic implementation of pipe()
. It uses generic tuples instead of overloading
to allow for an unlimited number of arguments.
Last active
October 21, 2021 01:33
-
-
Save tkburns/e07fa8466049cc95dadc78b21779080c to your computer and use it in GitHub Desktop.
Type safe, fully variadic implementation of pipe()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { pipe } from './pipe'; | |
// inferred as pipe<0, [1, 2, 3, 4]> | |
pipe( | |
(x: 0) => 1 as 1, | |
(x: 1) => 2 as 2, | |
(x: 2) => 3 as 3, | |
(x: 3) => 4 as 4, | |
); | |
/* | |
Unfortunately, any type error (with the arguments) is reported on the first argument | |
(due to them being rest parameters I think) | |
*/ | |
/* | |
Also, it strangely is able to infer the argument to the last function, but only the last function | |
*/ | |
// works | |
pipe( | |
(x: 0) => 1 as 1, | |
(x: 1) => 2 as 2, | |
(x: 2) => 3 as 3, | |
(x) => 4 as 4, | |
); | |
// type error, inferred as pipe<0, [1, 2, unknown, 4]> | |
/* | |
pipe( | |
(x: 0) => 1 as 1, | |
(x: 1) => 2 as 2, | |
(x) => 3 as 3, | |
(x: 3) => 4 as 4, | |
); | |
*/ | |
/* | |
Also, sadly it doesn't correctly infer the types if any of the args are generic functions | |
*/ | |
// type error, inferred as pipe<0, [1, unknown, 2]> | |
/* | |
pipe( | |
(x: 0) => 1 as 1, | |
<T>(x: T) => x, | |
(x: 1) => 2 as 2, | |
); | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { Head, Tail, Last, Prepend } from './tuple'; | |
type Fn<A, B> = (a: A) => B; | |
type PipeArgs<A, Ts extends unknown[]> = { | |
[K in keyof Ts]: K extends keyof Prepend<Ts, A> | |
? Fn<Prepend<Ts, A>[K], Ts[K]> | |
: never; | |
}; | |
type FirstArg<A> = [Fn<A, unknown>, ...unknown[]]; | |
export const pipe = <A, Ts extends unknown[]>(...fns: FirstArg<A> & PipeArgs<A, Ts>): Fn<A, Last<Ts>> => { | |
const pipeFns = fns as Fn<A | Ts[number], Ts[number]>[]; | |
const piped = pipeFns.reduce( | |
(piped, f) => (a) => f(piped(a)), | |
((a: A | Ts[number]) => a) | |
); | |
return piped as Fn<A, Last<Ts>>; | |
}; | |
/* Other implementations of pipe() */ | |
// using reduce, but reducing the value directly (inside of the returned function) | |
export const pipe2 = <A, Ts extends unknown[]>(...fns: FirstArg<A> & PipeArgs<A, Ts>): Fn<A, Last<Ts>> => { | |
return (a: A) => { | |
return (fns as Fn<A | Ts[number], Ts[number]>[]).reduce( | |
(step, f) => f(step), | |
a as A | Ts[number] | |
) as Last<Ts> | |
} | |
}; | |
// using a loop to construct a composed function | |
export const pipe3 = <A, Ts extends unknown[]>(...fns: FirstArg<A> & PipeArgs<A, Ts>): Fn<A, Last<Ts>> => { | |
let piped: Fn<A, A | Ts[number]> = (x) => x; | |
for (const f of fns) { | |
piped = (a: A) => { | |
return f(piped(a)) | |
}; | |
} | |
return piped as Fn<A, Last<Ts>>; | |
}; | |
// using a loop, but transforming the value directly (inside of the returned function) | |
export const pipe4 = <A, Ts extends unknown[]>(...fns: FirstArg<A> & PipeArgs<A, Ts>): Fn<A, Last<Ts>> => { | |
return (a: A) => { | |
let step: A | Ts[number] = a; | |
for (const f of fns) { | |
step = f(step); | |
} | |
return step as Last<Ts>; | |
} | |
}; | |
//using recursion to compose the functions together | |
const isNotEmpty = <T extends unknown>(l: T[]): l is [T, ...T[]] => l.length > 0; | |
export const pipe5 = <A, Ts extends unknown[]>(...fns: FirstArg<A> & PipeArgs<A, Ts>): Fn<A, Last<Ts>> => { | |
const [f, ...rest] = fns as [Fn<A, Head<Ts>>, ...Fn<Ts[number], Ts[number]>[]]; | |
if (isNotEmpty(rest)) { | |
const next = pipe<Head<Ts>, Ts[number][]>(...rest) as Fn<Head<Ts>, Last<Ts>>; | |
return (a: A) => next(f(a)); | |
} else { | |
return f as Fn<A, Last<Ts>>; | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type Head<L extends unknown[]> = | |
L extends [infer H, ...unknown[]] ? H : // tuple with >= 1 element | |
L extends [] ? never : // empty tuple | |
L[number] | undefined; // list with unknown length (eg x[]) | |
export type Tail<L extends unknown[]> = | |
L extends [head: unknown, ...tail: infer T] ? T : // tuple with >= 1 element | |
L extends [] ? [] : // empty tuple | |
L; // list with unknown length (eg x[]) | |
export type Last<L extends unknown[]> = | |
L extends Tail<L> ? (L[number] | undefined) : // list of unknown length (not a tuple) | |
L extends [] ? never : // empty tuple | |
L extends [unknown] ? Head<L> : // 1-element tuple | |
Last<Tail<L>>; // tuple with >= 2 elements | |
/* alternate definition for Last<L> */ | |
// export type Length<L extends unknown[]> = L extends { length: infer Length } | |
// ? Length | |
// : number; | |
// export type Last<L extends unknown[]> = [undefined, ...L][Length<L>]; | |
export type Prepend<L extends unknown[], T> = [T, ...L]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment