diff --git a/README.md b/README.md index 3918afb..75b8463 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,21 @@ const result = pipe( console.log(result); // "4" ``` +Or use `async` module to pipe a value through a series of asynchronous operator +functions. + +```ts +import { pipe } from "@core/pipe/async"; + +const result = pipe( + 1, + (v) => Promise.resolve(v + 1), // inferred as (v: number) => number | Promise + (v) => Promise.resolve(v * 2), // inferred as (v: number) => number | Promise + (v) => Promise.resolve(v.toString()), // inferred as (v: number) => string | Promise +); +console.log(result); // "4" +``` + ## License The code follows MIT license written in [LICENSE](./LICENSE). Contributors need diff --git a/async/mod.ts b/async/mod.ts new file mode 100644 index 0000000..dc62e19 --- /dev/null +++ b/async/mod.ts @@ -0,0 +1,490 @@ +/** + * AsyncOperator function that takes a value and returns a new value. + */ +export type AsyncOperator = (v: A) => B | Promise; + +/** + * @internal + */ +type LastAsyncOperatorReturn< + T extends AsyncOperator[], +> = T extends + [...AsyncOperator[], AsyncOperator] ? R + : never; + +/** + * Pipes a value through a series of asynchronous operator functions. + * Supports type inference for both the operator functions and the return value of the final operator. + * + * > [!NOTE] + * > + * > If the number of operators exceeds 20, the operator functions' types will default to + * > `AsyncOperator`, requiring explicit type annotations. + * + * @param value - The initial value to be processed through the operators. + * @param operators - A sequence of functions to apply to the value. + * @returns The final value after being processed through all the operators. + * + * @example + * ```ts + * import { pipe } from "@core/pipe/async"; + * + * const result = await pipe( + * 1, + * (v) => Promise.resolve(v + 1), // inferred as (v: number) => number | Promise + * (v) => Promise.resolve(v * 2), // inferred as (v: number) => number | Promise + * (v) => Promise.resolve(v.toString()), // inferred as (v: number) => string | Promise + * ); + * console.log(result); // "4" + * ``` + */ +export function pipe(value: V): Promise; +export function pipe( + value: V, + o01: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, +): Promise; +export function pipe( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, + o15: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, + o15: AsyncOperator, + o16: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, + o15: AsyncOperator, + o16: AsyncOperator, + o17: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, + o15: AsyncOperator, + o16: AsyncOperator, + o17: AsyncOperator, + o18: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, + T19, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, + o15: AsyncOperator, + o16: AsyncOperator, + o17: AsyncOperator, + o18: AsyncOperator, + o19: AsyncOperator, +): Promise; +export function pipe< + V, + T01, + T02, + T03, + T04, + T05, + T06, + T07, + T08, + T09, + T10, + T11, + T12, + T13, + T14, + T15, + T16, + T17, + T18, + T19, + T20, +>( + value: V, + o01: AsyncOperator, + o02: AsyncOperator, + o03: AsyncOperator, + o04: AsyncOperator, + o05: AsyncOperator, + o06: AsyncOperator, + o07: AsyncOperator, + o08: AsyncOperator, + o09: AsyncOperator, + o10: AsyncOperator, + o11: AsyncOperator, + o12: AsyncOperator, + o13: AsyncOperator, + o14: AsyncOperator, + o15: AsyncOperator, + o16: AsyncOperator, + o17: AsyncOperator, + o18: AsyncOperator, + o19: AsyncOperator, + o20: AsyncOperator, +): Promise; + +// deno-lint-ignore no-explicit-any +export function pipe[]>( + value: V, + ...operators: AsyncOperators +): Promise>; + +export async function pipe( + value: V, + // deno-lint-ignore no-explicit-any + ...operators: AsyncOperator[] +) { + return await operators.reduce( + async (result, next) => next(await result), + Promise.resolve(value), + ); +} diff --git a/async/mod_test.ts b/async/mod_test.ts new file mode 100644 index 0000000..f640b13 --- /dev/null +++ b/async/mod_test.ts @@ -0,0 +1,154 @@ +import { test } from "@cross/test"; +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { pipe } from "./mod.ts"; + +await test("pipe with no operators should return the input", async () => { + assertEquals(await pipe(1), 1); +}); + +await test("pipe with one operator should return operator applied value", async () => { + assertEquals(await pipe(1, (v) => Promise.resolve(v * 2)), 2); +}); + +await test("pipe with one operator should resolve the type properly", () => { + pipe(1, (v) => { + assertType>(true); + return v.toString(); + }); +}); + +await test("pipe with two operators should return operator applied value", async () => { + assertEquals( + await pipe(1, (v) => Promise.resolve(v * 2), (v) => Promise.resolve(v * 2)), + 4, + ); +}); + +await test("pipe with two operators should resolve the type properly", () => { + pipe(1, (v) => { + assertType>(true); + return v.toString(); + }, (v) => { + assertType>(true); + return v.length; + }); +}); + +await test("pipe with three operators should return operator applied value", async () => { + assertEquals( + await pipe( + 1, + (v) => Promise.resolve(v * 2), + (v) => Promise.resolve(v * 2), + (v) => Promise.resolve(v * 2), + ), + 8, + ); +}); + +await test("pipe with three operators should resolve the type properly", () => { + pipe(1, (v) => { + assertType>(true); + return v.toString(); + }, (v) => { + assertType>(true); + return v.length; + }, (v) => { + assertType>(true); + return v.toString(); + }); +}); + +await test("pipe with twenty operators should return operator applied value", async () => { + assertEquals( + await pipe(1, ...Array(20).fill((v: number) => Promise.resolve(v * 2))), + 2 ** 20, + ); +}); + +await test("pipe with twenty operators should resolve the type properly", () => { + pipe( + 1, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + (v) => { + assertType>(true); + return v; + }, + ); +}); diff --git a/deno.jsonc b/deno.jsonc index 973b122..5a3b249 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -2,7 +2,8 @@ "name": "@core/pipe", "version": "0.0.0", "exports": { - ".": "./mod.ts" + ".": "./mod.ts", + "./async": "./async/mod.ts" }, "exclude": [ ".coverage/**" @@ -21,6 +22,7 @@ }, "imports": { "@core/pipe": "./mod.ts", + "@core/pipe/async": "./async/mod.ts", "@cross/test": "jsr:@cross/test@^0.0.9", "@std/assert": "jsr:@std/assert@^1.0.2", "@std/jsonc": "jsr:@std/jsonc@^1.0.0", diff --git a/mod.ts b/mod.ts index 2d116f5..0e0c09c 100644 --- a/mod.ts +++ b/mod.ts @@ -4,11 +4,11 @@ export type Operator = (v: A) => B; /** - * Get the return type of the last operator in a list of operators. + * @internal */ -export type LastOperatorReturn[]> = - T extends [...Operator[], Operator] ? R - : never; +type LastOperatorReturn[]> = T extends + [...Operator[], Operator] ? R + : never; /** * Pipes a value through a series of operator functions.