Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"./fmt",
"./front_matter",
"./fs",
"./functions",
"./html",
"./http",
"./ini",
Expand Down
8 changes: 8 additions & 0 deletions functions/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@std/functions",
"version": "0.1.0",
"exports": {
".": "./mod.ts",
"./pipe": "./pipe.ts"
}
}
23 changes: 23 additions & 0 deletions functions/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2018-2025 the Deno authors. MIT license.

// This module is browser compatible.

/**
* Utilities for working with functions.
*
* ```ts
* import { pipe } from "@std/functions";
* import { assertEquals } from "@std/assert";
*
* const myPipe = pipe(
* Math.abs,
* Math.sqrt,
* Math.floor,
* (num: number) => `result: ${num}`,
* );
* assertEquals(myPipe(-2), "result: 1");
* ```
*
* @module
*/
export * from "./pipe.ts";
57 changes: 57 additions & 0 deletions functions/pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2018-2025 the Deno authors. MIT license.

// deno-lint-ignore-file no-explicit-any
type AnyFunc = (...arg: any) => any;

type LastFnReturnType<F extends Array<AnyFunc>, Else = never> = F extends [
...any[],
(...arg: any) => infer R,
] ? R
: Else;

// inspired by https://dev.to/ecyrbe/how-to-use-advanced-typescript-to-define-a-pipe-function-381h
type PipeArgs<F extends AnyFunc[], Acc extends AnyFunc[] = []> = F extends [
(...args: infer A) => infer B,
] ? [...Acc, (...args: A) => B]
: F extends [(...args: infer A) => any, ...infer Tail]
? Tail extends [(arg: infer B) => any, ...any[]]
? PipeArgs<Tail, [...Acc, (...args: A) => B]>
: Acc
: Acc;

/**
* Composes functions from left to right, the output of each function is the input for the next.
*
* @example Usage
* ```ts
* import { assertEquals } from "@std/assert";
* import { pipe } from "@std/functions";
*
* const myPipe = pipe(
* Math.abs,
* Math.sqrt,
* Math.floor,
* (num: number) => `result: ${num}`,
* );
* assertEquals(myPipe(-2), "result: 1");
* ```
*
* @param input The functions to be composed
* @returns A function composed of the input functions, from left to right
*/
export function pipe(): <T>(arg: T) => T;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this first overload? What is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is my habit that if an edge case has a natural interpretation I include it by default, even without a concrete use-case. This isn't a strong opinion, happy to remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would note that a scenario for this will, of course, involve .apply(). Again, I don't mind.

export function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
firstFn: FirstFn,
...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
): (arg: Parameters<FirstFn>[0]) => LastFnReturnType<F, ReturnType<FirstFn>>;

export function pipe<FirstFn extends AnyFunc, F extends AnyFunc[]>(
firstFn?: FirstFn,
...fns: PipeArgs<F> extends F ? F : PipeArgs<F>
): any {
if (!firstFn) {
return <T>(arg: T) => arg;
}
return (arg: Parameters<FirstFn>[0]) =>
(fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg));
}
32 changes: 32 additions & 0 deletions functions/pipe_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2018-2025 the Deno authors. MIT license.

import { assertEquals, assertThrows } from "@std/assert";
import { pipe } from "./pipe.ts";

Deno.test("pipe() handles mixed types", () => {
const inputPipe = pipe(
Math.abs,
Math.sqrt,
Math.floor,
(num: number) => `result: ${num}`,
);
assertEquals(inputPipe(-2), "result: 1");
});

Deno.test("en empty pipe is the identity function", () => {
const inputPipe = pipe();
assertEquals(inputPipe("hello"), "hello");
});

Deno.test("pipe() throws an exceptions when a function throws an exception", () => {
const inputPipe = pipe(
Math.abs,
Math.sqrt,
Math.floor,
(num: number) => {
throw new Error("This is an error for " + num);
},
(num: number) => `result: ${num}`,
);
assertThrows(() => inputPipe(-2));
});
1 change: 1 addition & 0 deletions import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@std/expect": "jsr:@std/expect@^1.0.11",
"@std/fmt": "jsr:@std/fmt@^1.0.4",
"@std/front-matter": "jsr:@std/front-matter@^1.0.5",
"@std/functions": "jsr:@std/functions@^0.1.0",
"@std/fs": "jsr:@std/fs@^1.0.9",
"@std/html": "jsr:@std/html@^1.0.3",
"@std/http": "jsr:@std/http@^1.0.12",
Expand Down
Loading