diff --git a/benchmarks/bundle-size/fp/flowAsync.spec.ts b/benchmarks/bundle-size/fp/flowAsync.spec.ts new file mode 100644 index 000000000..d24198ce5 --- /dev/null +++ b/benchmarks/bundle-size/fp/flowAsync.spec.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import { getBundleSizeFromScript } from '../utils/getBundleSize'; + +describe('fp/flowAsync bundle size', () => { + it('es-toolkit/fp', async () => { + expect( + await getBundleSizeFromScript('import { flowAsync } from "es-toolkit/fp"; console.log(flowAsync)') + ).toMatchInlineSnapshot(`194`); + }); +}); diff --git a/docs/fp/reference/flowAsync.md b/docs/fp/reference/flowAsync.md new file mode 100644 index 000000000..cc93a5079 --- /dev/null +++ b/docs/fp/reference/flowAsync.md @@ -0,0 +1,66 @@ +# flowAsync (Functional Programming) + +Performs left-to-right async function composition, returning a reusable async function that awaits every intermediate result. + +```typescript +const fn = flowAsync(...functions); +const result = await fn(...args); +``` + +::: info + +`flowAsync` is the promise-aware sibling of [`flow`](./flow.md). `flow` passes each raw return value straight to the next function, so a returned `Promise` arrives unresolved; `flowAsync` awaits every step, letting synchronous and asynchronous functions mix freely in one chain. + +::: + +## Usage + +`flowAsync` takes a sequence of functions and composes them left-to-right into a single async function. The first function may take any number of arguments; every later function is unary and receives the awaited result of the previous one. The composed function always returns a `Promise`. + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const fetchUser = async (id: number) => ({ id, name: 'Alice' }); +const getName = (user: { name: string }) => user.name; + +const getUserName = flowAsync(fetchUser, getName); + +await getUserName(1); // => 'Alice' +``` + +Synchronous and asynchronous functions can be mixed in any order; each result is awaited before it is passed on. + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const process = flowAsync( + (x: number) => x + 1, + async x => x * 3, + x => `value: ${x}` +); + +await process(1); // => 'value: 6' +``` + +If any function throws or returns a rejected `Promise`, the composed function rejects with that error, so a single `try`/`catch` (or `.catch`) around the call handles failures from every step. + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const risky = flowAsync( + async (id: number) => { + throw new Error(`user ${id} not found`); + }, + (user: { name: string }) => user.name +); + +await risky(1); // rejects with Error: user 1 not found +``` + +#### Parameters + +- `functions`: The functions to compose, from left to right. The first may take any number of arguments; the rest are unary, each receiving the awaited output of the previous function. + +#### Returns + +A new async function that applies every function in sequence, awaiting each result. It accepts the same parameters as the first function and resolves to the awaited result of the last. The public overloads infer the precise types from the chain. diff --git a/docs/ja/fp/reference/flowAsync.md b/docs/ja/fp/reference/flowAsync.md new file mode 100644 index 000000000..0c67886d5 --- /dev/null +++ b/docs/ja/fp/reference/flowAsync.md @@ -0,0 +1,66 @@ +# flowAsync (関数型プログラミング) + +左から右へ関数を非同期に合成し、各中間結果を待機する再利用可能な非同期関数を返します。 + +```typescript +const fn = flowAsync(...functions); +const result = await fn(...args); +``` + +::: info + +`flowAsync` は [`flow`](./flow.md) の Promise 対応版です。`flow` は各関数の戻り値をそのまま次の関数に渡すため、`Promise` を返すと未解決のまま渡されてしまいます。`flowAsync` はすべてのステップを待機するため、同期関数と非同期関数を 1 つのチェーンで自由に混在させられます。 + +::: + +## 使用法 + +`flowAsync` は一連の関数を受け取り、左から右へ 1 つの非同期関数に合成します。最初の関数は任意の数の引数を受け取れますが、それ以降の関数はすべて単項で、直前の関数の結果を待機した値を受け取ります。合成された関数は常に `Promise` を返します。 + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const fetchUser = async (id: number) => ({ id, name: 'Alice' }); +const getName = (user: { name: string }) => user.name; + +const getUserName = flowAsync(fetchUser, getName); + +await getUserName(1); // => 'Alice' +``` + +同期関数と非同期関数は任意の順序で混在できます。各結果は次の関数に渡される前に待機されます。 + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const process = flowAsync( + (x: number) => x + 1, + async x => x * 3, + x => `value: ${x}` +); + +await process(1); // => 'value: 6' +``` + +いずれかの関数がエラーを投げたり、拒否された `Promise` を返したりすると、合成された関数もそのエラーで拒否されます。呼び出しを 1 つの `try`/`catch`(または `.catch`)で囲めば、すべてのステップの失敗を処理できます。 + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const risky = flowAsync( + async (id: number) => { + throw new Error(`user ${id} not found`); + }, + (user: { name: string }) => user.name +); + +await risky(1); // Error: user 1 not found で拒否されます。 +``` + +#### パラメータ + +- `functions`: 左から右へ合成する関数です。最初の関数は任意の数の引数を受け取れますが、残りはすべて単項で、直前の関数の出力を待機した値を受け取ります。 + +#### 戻り値 + +すべての関数を順番に適用し、各結果を待機する新しい非同期関数です。最初の関数と同じパラメータを受け取り、最後の関数の結果を待機した値で解決されます。公開されているオーバーロードは、チェーンから正確な型を推論します。 diff --git a/docs/ko/fp/reference/flowAsync.md b/docs/ko/fp/reference/flowAsync.md new file mode 100644 index 000000000..9fe6f2099 --- /dev/null +++ b/docs/ko/fp/reference/flowAsync.md @@ -0,0 +1,66 @@ +# flowAsync (함수형 프로그래밍) + +왼쪽에서 오른쪽으로 함수를 비동기로 합성해서, 매 중간 결과를 기다리는 재사용 가능한 비동기 함수를 반환해요. + +```typescript +const fn = flowAsync(...functions); +const result = await fn(...args); +``` + +::: info + +`flowAsync`는 [`flow`](./flow.md)의 Promise 인식 버전이에요. `flow`는 각 함수의 반환값을 그대로 다음 함수에 넘기기 때문에, 어떤 함수가 `Promise`를 반환하면 `await`되지 않은 상태로 다음 함수에 전달돼요. `flowAsync`는 매 단계를 기다려서, 동기 함수와 비동기 함수를 한 체인에서 자유롭게 섞어 쓸 수 있어요. + +::: + +## 사용법 + +`flowAsync`는 여러 함수를 받아서 왼쪽에서 오른쪽으로 하나의 비동기 함수로 합성해요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 그 뒤의 함수들은 모두 인자를 하나 받아서 이전 함수의 결과를 기다린 값을 넘겨받아요. 합성된 함수는 항상 `Promise`를 반환해요. + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const fetchUser = async (id: number) => ({ id, name: 'Alice' }); +const getName = (user: { name: string }) => user.name; + +const getUserName = flowAsync(fetchUser, getName); + +await getUserName(1); // => 'Alice' +``` + +동기 함수와 비동기 함수를 어떤 순서로든 섞을 수 있어요. 각 결과는 다음 함수로 넘어가기 전에 기다려져요. + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const process = flowAsync( + (x: number) => x + 1, + async x => x * 3, + x => `value: ${x}` +); + +await process(1); // => 'value: 6' +``` + +어떤 함수든 에러를 던지거나 거부된 `Promise`를 반환하면, 합성된 함수도 그 에러로 거부돼요. 호출부를 `try`/`catch`(또는 `.catch`) 하나로 감싸면 모든 단계의 실패를 처리할 수 있어요. + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const risky = flowAsync( + async (id: number) => { + throw new Error(`user ${id} not found`); + }, + (user: { name: string }) => user.name +); + +await risky(1); // Error: user 1 not found 로 거부돼요. +``` + +#### 파라미터 + +- `functions`: 왼쪽에서 오른쪽으로 합성할 함수들이에요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 나머지는 모두 인자를 하나 받아서 이전 함수의 출력을 기다린 값을 넘겨받아요. + +#### 반환 값 + +모든 함수를 순서대로 적용하면서 각 결과를 기다리는 새로운 비동기 함수예요. 첫 번째 함수와 같은 파라미터를 받아서 마지막 함수의 결과를 기다린 값으로 해결돼요. 공개된 오버로드는 체인에서 정확한 타입을 추론해요. diff --git a/docs/zh_hans/fp/reference/flowAsync.md b/docs/zh_hans/fp/reference/flowAsync.md new file mode 100644 index 000000000..aa5abdc59 --- /dev/null +++ b/docs/zh_hans/fp/reference/flowAsync.md @@ -0,0 +1,66 @@ +# flowAsync (函数式编程) + +执行从左到右的异步函数组合,返回一个会等待每个中间结果的可复用异步函数。 + +```typescript +const fn = flowAsync(...functions); +const result = await fn(...args); +``` + +::: info + +`flowAsync` 是 [`flow`](./flow.md) 的 Promise 感知版本。`flow` 会将每个函数的原始返回值直接传给下一个函数,因此返回的 `Promise` 会以未解决的状态被传递;`flowAsync` 会等待每一步,让同步函数和异步函数可以在一条链中自由混用。 + +::: + +## 用法 + +`flowAsync` 接收一系列函数,并将它们从左到右组合成一个异步函数。第一个函数可以接收任意数量的参数,其后的每个函数都是一元的,接收上一个函数结果被等待后的值。组合出的函数始终返回 `Promise`。 + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const fetchUser = async (id: number) => ({ id, name: 'Alice' }); +const getName = (user: { name: string }) => user.name; + +const getUserName = flowAsync(fetchUser, getName); + +await getUserName(1); // => 'Alice' +``` + +同步函数和异步函数可以以任意顺序混用;每个结果在传给下一个函数之前都会被等待。 + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const process = flowAsync( + (x: number) => x + 1, + async x => x * 3, + x => `value: ${x}` +); + +await process(1); // => 'value: 6' +``` + +如果任何函数抛出错误或返回被拒绝的 `Promise`,组合出的函数就会以该错误拒绝。只需在调用处使用一个 `try`/`catch`(或 `.catch`),就能处理每一步的失败。 + +```typescript +import { flowAsync } from 'es-toolkit/fp'; + +const risky = flowAsync( + async (id: number) => { + throw new Error(`user ${id} not found`); + }, + (user: { name: string }) => user.name +); + +await risky(1); // 以 Error: user 1 not found 拒绝 +``` + +#### 参数 + +- `functions`: 从左到右组合的函数。第一个函数可以接收任意数量的参数,其余函数都是一元的,接收上一个函数输出被等待后的值。 + +#### 返回值 + +一个依次应用所有函数并等待每个结果的新异步函数。它接收与第一个函数相同的参数,并解决为最后一个函数结果被等待后的值。公开的重载会从整条链中推断出精确的类型。 diff --git a/src/fp/flowAsync.spec.ts b/src/fp/flowAsync.spec.ts new file mode 100644 index 000000000..ef017d022 --- /dev/null +++ b/src/fp/flowAsync.spec.ts @@ -0,0 +1,124 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; +import { filter } from './array/filter.ts'; +import { map } from './array/map.ts'; +import { take } from './array/take.ts'; +import { flowAsync } from './flowAsync.ts'; + +describe('flowAsync', () => { + it('resolves to its single argument unchanged when no functions are given', async () => { + // No public overload accepts zero functions, but the runtime is defined for + // it; cast to exercise the identity behaviour. + const identity = (flowAsync as () => (value: number) => Promise)(); + await expect(identity(42)).resolves.toBe(42); + }); + + it('awaits a promise before passing it to the next function', async () => { + const fetchUser = async (id: number) => ({ id, name: 'Alice' }); + const getName = (user: { name: string }) => user.name; + + const getUserName = flowAsync(fetchUser, getName); + + await expect(getUserName(1)).resolves.toBe('Alice'); + }); + + it('lets the first function take multiple arguments', async () => { + const combined = flowAsync( + async (x: number, y: number) => x + y, + (n: number) => n * n + ); + + await expect(combined(1, 2)).resolves.toBe(9); + }); + + it('composes several sync functions into an async function', async () => { + const combined = flowAsync( + (x: number) => x + 1, + x => x * 3, + x => `value: ${x}` + ); + + await expect(combined(1)).resolves.toBe('value: 6'); + }); + + it('mixes sync and async functions freely', async () => { + const combined = flowAsync( + (x: number) => x + 1, + async x => x * 3, + x => `value: ${x}`, + async s => s.toUpperCase() + ); + + await expect(combined(1)).resolves.toBe('VALUE: 6'); + }); + + it('awaits every step in order', async () => { + const calls: string[] = []; + + const combined = flowAsync( + async (x: number) => { + await new Promise(resolve => setTimeout(resolve, 10)); + calls.push('first'); + return x + 1; + }, + async x => { + calls.push('second'); + return x * 2; + } + ); + + await expect(combined(1)).resolves.toBe(4); + expect(calls).toEqual(['first', 'second']); + }); + + it('works with es-toolkit/fp operators', async () => { + const pipeline = flowAsync( + async (data: number[]) => data, + map((x: number) => x * x), + filter(x => x % 2 === 0), + take(2) + ); + + await expect(pipeline([1, 2, 3, 4, 5, 6, 7, 8])).resolves.toEqual([4, 16]); + }); + + it('rejects when a composed function throws', async () => { + const combined = flowAsync( + async (x: number) => x + 1, + () => { + throw new Error('boom'); + } + ); + + await expect(combined(1)).rejects.toThrow('boom'); + }); + + it('rejects when a composed function returns a rejected promise', async () => { + const combined = flowAsync( + (x: number) => x + 1, + async () => { + throw new Error('boom'); + } + ); + + await expect(combined(1)).rejects.toThrow('boom'); + }); + + it('returns a reusable function that can be called many times', async () => { + const double = flowAsync(async (x: number) => x * 2); + + await expect(double(1)).resolves.toBe(2); + await expect(double(2)).resolves.toBe(4); + await expect(double(3)).resolves.toBe(6); + }); + + it('infers the first function parameters and the awaited final return type', () => { + const combined = flowAsync( + async (x: number, y: string) => x + y.length, + (n: number) => n > 0, + async (b: boolean): Promise => (b ? 'yes' : 'no') + ); + + expectTypeOf(combined).parameters.toEqualTypeOf<[number, string]>(); + expectTypeOf(combined).returns.toEqualTypeOf>(); + }); +}); diff --git a/src/fp/flowAsync.ts b/src/fp/flowAsync.ts new file mode 100644 index 000000000..8cafb8c1d --- /dev/null +++ b/src/fp/flowAsync.ts @@ -0,0 +1,384 @@ +/** + * Creates a reusable async function from a single function. + * + * @param fn1 - The first function, which may take any number of arguments. + * @returns An async function with the same parameters as `fn1` that resolves to its awaited result. + */ +export function flowAsync(fn1: (...args: A) => R1): (...args: A) => Promise>; +/** + * Composes `fn1` and `fn2` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn2`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn3` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn3`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn4` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn4`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn5` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn5`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn6` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn6`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn7` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn7`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn8` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn8`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn9` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn9`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn10` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @param fn10 - Applied to the awaited result of `fn9`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn10`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9, + fn10: (input: Awaited) => R10 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn11` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @param fn10 - Applied to the awaited result of `fn9`. + * @param fn11 - Applied to the awaited result of `fn10`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn11`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9, + fn10: (input: Awaited) => R10, + fn11: (input: Awaited) => R11 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn12` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @param fn10 - Applied to the awaited result of `fn9`. + * @param fn11 - Applied to the awaited result of `fn10`. + * @param fn12 - Applied to the awaited result of `fn11`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn12`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9, + fn10: (input: Awaited) => R10, + fn11: (input: Awaited) => R11, + fn12: (input: Awaited) => R12 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn13` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @param fn10 - Applied to the awaited result of `fn9`. + * @param fn11 - Applied to the awaited result of `fn10`. + * @param fn12 - Applied to the awaited result of `fn11`. + * @param fn13 - Applied to the awaited result of `fn12`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn13`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9, + fn10: (input: Awaited) => R10, + fn11: (input: Awaited) => R11, + fn12: (input: Awaited) => R12, + fn13: (input: Awaited) => R13 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn14` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @param fn10 - Applied to the awaited result of `fn9`. + * @param fn11 - Applied to the awaited result of `fn10`. + * @param fn12 - Applied to the awaited result of `fn11`. + * @param fn13 - Applied to the awaited result of `fn12`. + * @param fn14 - Applied to the awaited result of `fn13`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn14`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9, + fn10: (input: Awaited) => R10, + fn11: (input: Awaited) => R11, + fn12: (input: Awaited) => R12, + fn13: (input: Awaited) => R13, + fn14: (input: Awaited) => R14 +): (...args: A) => Promise>; +/** + * Composes `fn1` through `fn15` left-to-right into a single reusable async function, awaiting each result. + * + * @param fn1 - The first function, which may take any number of arguments. + * @param fn2 - Applied to the awaited result of `fn1`. + * @param fn3 - Applied to the awaited result of `fn2`. + * @param fn4 - Applied to the awaited result of `fn3`. + * @param fn5 - Applied to the awaited result of `fn4`. + * @param fn6 - Applied to the awaited result of `fn5`. + * @param fn7 - Applied to the awaited result of `fn6`. + * @param fn8 - Applied to the awaited result of `fn7`. + * @param fn9 - Applied to the awaited result of `fn8`. + * @param fn10 - Applied to the awaited result of `fn9`. + * @param fn11 - Applied to the awaited result of `fn10`. + * @param fn12 - Applied to the awaited result of `fn11`. + * @param fn13 - Applied to the awaited result of `fn12`. + * @param fn14 - Applied to the awaited result of `fn13`. + * @param fn15 - Applied to the awaited result of `fn14`. + * @returns An async function with the same parameters as `fn1` that resolves to the awaited result of `fn15`. + */ +export function flowAsync( + fn1: (...args: A) => R1, + fn2: (input: Awaited) => R2, + fn3: (input: Awaited) => R3, + fn4: (input: Awaited) => R4, + fn5: (input: Awaited) => R5, + fn6: (input: Awaited) => R6, + fn7: (input: Awaited) => R7, + fn8: (input: Awaited) => R8, + fn9: (input: Awaited) => R9, + fn10: (input: Awaited) => R10, + fn11: (input: Awaited) => R11, + fn12: (input: Awaited) => R12, + fn13: (input: Awaited) => R13, + fn14: (input: Awaited) => R14, + fn15: (input: Awaited) => R15 +): (...args: A) => Promise>; +/** + * Performs left-to-right async function composition, returning a **reusable + * async function** instead of running immediately. The first function may take + * any number of arguments; every later function is unary and receives the + * awaited result of the previous one. + * + * `flowAsync` is the promise-aware sibling of {@link flow}: where `flow` passes + * each raw return value straight to the next function (so a returned `Promise` + * arrives unresolved), `flowAsync` awaits every step, letting synchronous and + * asynchronous functions mix freely in one chain. The composed function always + * returns a `Promise`. + * + * @param functions - The functions to compose. The first may be variadic; the + * rest are unary, each receiving the awaited output of the previous function. + * @returns An async function that, when called, applies every function + * left-to-right, awaiting each result. + * + * @example + * import { flowAsync } from 'es-toolkit/fp'; + * + * const fetchUser = async (id: number) => ({ id, name: 'Alice' }); + * const getName = (user: { name: string }) => user.name; + * + * const getUserName = flowAsync(fetchUser, getName); + * await getUserName(1); // => 'Alice' + */ +export function flowAsync(...functions: Array<(...args: any[]) => any>): (...args: any[]) => Promise { + return async function (this: any, ...args: any[]): Promise { + if (functions.length === 0) { + return args[0]; + } + + let result = await functions[0].apply(this, args); + + for (let i = 1; i < functions.length; i++) { + result = await functions[i].call(this, result); + } + + return result; + }; +} diff --git a/src/fp/index.spec.ts b/src/fp/index.spec.ts index 6c11be02c..c60a5f750 100644 --- a/src/fp/index.spec.ts +++ b/src/fp/index.spec.ts @@ -1,9 +1,39 @@ import { describe, expect, it } from 'vitest'; -import { add, chunk, filter, flatMap, flow, map, multiply, omit, pick, pipe, sortBy, take, uniq } from './index.ts'; +import { + add, + chunk, + filter, + flatMap, + flow, + flowAsync, + map, + multiply, + omit, + pick, + pipe, + sortBy, + take, + uniq, +} from './index.ts'; describe('es-toolkit/fp', () => { it('exports pipe and every v1 operator from the public entry', () => { - for (const fn of [pipe, flow, map, filter, flatMap, take, uniq, chunk, sortBy, multiply, add, pick, omit]) { + for (const fn of [ + pipe, + flow, + flowAsync, + map, + filter, + flatMap, + take, + uniq, + chunk, + sortBy, + multiply, + add, + pick, + omit, + ]) { expect(typeof fn).toBe('function'); } }); diff --git a/src/fp/index.ts b/src/fp/index.ts index 00ea0fcd0..1d4c8e98d 100644 --- a/src/fp/index.ts +++ b/src/fp/index.ts @@ -1,5 +1,6 @@ export * from './array/index.ts'; export { flow } from './flow.ts'; +export { flowAsync } from './flowAsync.ts'; export * from './math/index.ts'; export * from './object/index.ts'; export { pipe } from './pipe.ts';