Skip to content

Instantly share code, notes, and snippets.

@tkburns
Last active October 21, 2021 01:33
Show Gist options
  • Save tkburns/e07fa8466049cc95dadc78b21779080c to your computer and use it in GitHub Desktop.
Save tkburns/e07fa8466049cc95dadc78b21779080c to your computer and use it in GitHub Desktop.
Type safe, fully variadic implementation of pipe()

Type Safe and Fully Variadic pipe()

A type safe and fully variadic implementation of pipe(). It uses generic tuples instead of overloading to allow for an unlimited number of arguments.

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,
);
*/
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>>;
}
};
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