Skip to content

Instantly share code, notes, and snippets.

@joshcox
Created September 19, 2018 18:10
Show Gist options
  • Save joshcox/d59bf241458cf456b49f3c5b098af9fa to your computer and use it in GitHub Desktop.
Save joshcox/d59bf241458cf456b49f3c5b098af9fa to your computer and use it in GitHub Desktop.
Type Safe Function Composition in TypeScript
// Get the type of the first argument of a function
type ArgumentType<T> = T extends (a: infer U) => any ? U : any;
// Get the head type from a tuple of types
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
// Get the tail type from a tuple of types
type Tail<T extends any[]> = ((...t: T) => void) extends ((h: any, ...rest: infer R) => void) ? R : never;
// Get the Last type from a tuple of types
type Last<T extends any[]> = Last_<T>;
type Last_<T extends any[]> = {
0: Head<T>,
1: Last_<Tail<T>>
}[T extends [any] ? 0 : 1];
// Create the Composed function signature by creating a function type where
// the input is the argument type of the last function and the output is the return type
// of the first function
type ComposeSignature<FS extends Array<(a: any) => any>> =
(arg: ArgumentType<Last<FS>>) =>
FS extends [infer HEAD, ...any[]]
? HEAD extends (a: any) => any
? ReturnType<HEAD>
: never
: never;
// Validate passthrough/intermediate types
// compose(string -> boolean, number -> string) - will ensure that the itermediate value exchanges type correctly
type Composed<FS extends Array<(a: any) => any>> = Composed_<FS, FS>;
type Composed_<S extends Array<(a: any) => any>, T extends Array<(a: any) => any>> = {
0: ComposeSignature<S>,
1: Head<Tail<T>> extends (a: any) => infer R
? ArgumentType<Head<T>> extends R
? Composed_<S, Tail<T>>
: never
: never
}[T extends [any] ? 0 : 1];
function compose(): <A>(arg: A) => A;
function compose<F extends (arg: any) => any>(f: F): F;
function compose<FS extends Array<(arg: any) => any>>(...fns: FS): Composed<FS>
function compose(...fns) {
return (arg) => fns.reduce((a, f) => f(a), arg);
}
const echo = compose();
const b = echo("string");
const square = compose((x: number): number => x * x);
const sq = square(2);
const strToSquare = compose(
(x: number) => x * x,
(t: string) => parseInt(t, 10),
);
const s1 = strToSquare("1");
const s2 = strToSquare("2");
const bazbar = compose(
(x: number): number => x * 2,
(x: string): number => parseInt(x, 10),
(b: boolean): string => b ? "1" : "0"
);
const v1 = bazbar(true);
const v2 = bazbar(false);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment