From ee622b58e5eba682f2efb0011094a370c394f34c Mon Sep 17 00:00:00 2001 From: Sojin Park Date: Mon, 17 Aug 2026 19:55:02 +0900 Subject: [PATCH 1/4] feat(fp): add flowAsync, promise-aware left-to-right composition Composes functions left-to-right into a reusable async function that awaits every intermediate result, so synchronous and asynchronous functions can be mixed freely in one chain. Closes #1658 --- benchmarks/bundle-size/fp/flowAsync.spec.ts | 10 + docs/fp/reference/flowAsync.md | 66 ++++ docs/ja/fp/reference/flowAsync.md | 66 ++++ docs/ko/fp/reference/flowAsync.md | 66 ++++ docs/zh_hans/fp/reference/flowAsync.md | 66 ++++ src/fp/flowAsync.spec.ts | 124 +++++++ src/fp/flowAsync.ts | 384 ++++++++++++++++++++ src/fp/index.spec.ts | 34 +- src/fp/index.ts | 1 + 9 files changed, 815 insertions(+), 2 deletions(-) create mode 100644 benchmarks/bundle-size/fp/flowAsync.spec.ts create mode 100644 docs/fp/reference/flowAsync.md create mode 100644 docs/ja/fp/reference/flowAsync.md create mode 100644 docs/ko/fp/reference/flowAsync.md create mode 100644 docs/zh_hans/fp/reference/flowAsync.md create mode 100644 src/fp/flowAsync.spec.ts create mode 100644 src/fp/flowAsync.ts 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..a891ef9e8 --- /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` (`Array<(...args: any[]) => any>`): 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 + +(`(...args: any[]) => Promise`): 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..524e54bbb --- /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` (`Array<(...args: any[]) => any>`): 左から右へ合成する関数です。最初の関数は任意の数の引数を受け取れますが、残りはすべて単項で、直前の関数の出力を待機した値を受け取ります。 + +#### 戻り値 + +(`(...args: any[]) => Promise`): すべての関数を順番に適用し、各結果を待機する新しい非同期関数です。最初の関数と同じパラメータを受け取り、最後の関数の結果を待機した値で解決されます。公開されているオーバーロードは、チェーンから正確な型を推論します。 diff --git a/docs/ko/fp/reference/flowAsync.md b/docs/ko/fp/reference/flowAsync.md new file mode 100644 index 000000000..880f40178 --- /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`를 반환하면 해결되지 않은 채로 전달돼요. `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` (`Array<(...args: any[]) => any>`): 왼쪽에서 오른쪽으로 합성할 함수들이에요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 나머지는 모두 인자를 하나 받아서 이전 함수의 출력을 기다린 값을 넘겨받아요. + +#### 반환 값 + +(`(...args: any[]) => Promise`): 모든 함수를 순서대로 적용하면서 각 결과를 기다리는 새로운 비동기 함수예요. 첫 번째 함수와 같은 파라미터를 받아서 마지막 함수의 결과를 기다린 값으로 해결돼요. 공개된 오버로드는 체인에서 정확한 타입을 추론해요. diff --git a/docs/zh_hans/fp/reference/flowAsync.md b/docs/zh_hans/fp/reference/flowAsync.md new file mode 100644 index 000000000..74a35b750 --- /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` (`Array<(...args: any[]) => any>`): 从左到右组合的函数。第一个函数可以接收任意数量的参数,其余函数都是一元的,接收上一个函数输出被等待后的值。 + +#### 返回值 + +(`(...args: any[]) => Promise`): 一个依次应用所有函数并等待每个结果的新异步函数。它接收与第一个函数相同的参数,并解决为最后一个函数结果被等待后的值。公开的重载会从整条链中推断出精确的类型。 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'; From e6596cd607ce4fd07d0cdc94cc04bd9343bd9ff6 Mon Sep 17 00:00:00 2001 From: Sojin Park Date: Wed, 19 Aug 2026 09:25:12 +0900 Subject: [PATCH 2/4] docs(ko): clarify the unresolved Promise wording in flowAsync --- docs/ko/fp/reference/flowAsync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ko/fp/reference/flowAsync.md b/docs/ko/fp/reference/flowAsync.md index 880f40178..e5d86c0d0 100644 --- a/docs/ko/fp/reference/flowAsync.md +++ b/docs/ko/fp/reference/flowAsync.md @@ -9,7 +9,7 @@ const result = await fn(...args); ::: info -`flowAsync`는 [`flow`](./flow.md)의 Promise 인식 버전이에요. `flow`는 각 함수의 반환값을 그대로 다음 함수에 넘기기 때문에 `Promise`를 반환하면 해결되지 않은 채로 전달돼요. `flowAsync`는 매 단계를 기다려서, 동기 함수와 비동기 함수를 한 체인에서 자유롭게 섞어 쓸 수 있어요. +`flowAsync`는 [`flow`](./flow.md)의 Promise 인식 버전이에요. `flow`는 각 함수의 반환값을 그대로 다음 함수에 넘기기 때문에, 어떤 함수가 `Promise`를 반환하면 `await`되지 않은 상태로 다음 함수에 전달돼요. `flowAsync`는 매 단계를 기다려서, 동기 함수와 비동기 함수를 한 체인에서 자유롭게 섞어 쓸 수 있어요. ::: From 59bef16d65d663baefacaa02281d244af68d1b02 Mon Sep 17 00:00:00 2001 From: Sojin Park Date: Sat, 22 Aug 2026 17:35:24 +0900 Subject: [PATCH 3/4] docs(fp): remove type annotations from flowAsync parameter docs --- docs/fp/reference/flowAsync.md | 2 +- docs/ja/fp/reference/flowAsync.md | 2 +- docs/ko/fp/reference/flowAsync.md | 2 +- docs/zh_hans/fp/reference/flowAsync.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/fp/reference/flowAsync.md b/docs/fp/reference/flowAsync.md index a891ef9e8..a442e667c 100644 --- a/docs/fp/reference/flowAsync.md +++ b/docs/fp/reference/flowAsync.md @@ -59,7 +59,7 @@ await risky(1); // rejects with Error: user 1 not found #### Parameters -- `functions` (`Array<(...args: any[]) => any>`): 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. +- `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 diff --git a/docs/ja/fp/reference/flowAsync.md b/docs/ja/fp/reference/flowAsync.md index 524e54bbb..884cc35f5 100644 --- a/docs/ja/fp/reference/flowAsync.md +++ b/docs/ja/fp/reference/flowAsync.md @@ -59,7 +59,7 @@ await risky(1); // Error: user 1 not found で拒否されます。 #### パラメータ -- `functions` (`Array<(...args: any[]) => any>`): 左から右へ合成する関数です。最初の関数は任意の数の引数を受け取れますが、残りはすべて単項で、直前の関数の出力を待機した値を受け取ります。 +- `functions`: 左から右へ合成する関数です。最初の関数は任意の数の引数を受け取れますが、残りはすべて単項で、直前の関数の出力を待機した値を受け取ります。 #### 戻り値 diff --git a/docs/ko/fp/reference/flowAsync.md b/docs/ko/fp/reference/flowAsync.md index e5d86c0d0..24f051073 100644 --- a/docs/ko/fp/reference/flowAsync.md +++ b/docs/ko/fp/reference/flowAsync.md @@ -59,7 +59,7 @@ await risky(1); // Error: user 1 not found 로 거부돼요. #### 파라미터 -- `functions` (`Array<(...args: any[]) => any>`): 왼쪽에서 오른쪽으로 합성할 함수들이에요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 나머지는 모두 인자를 하나 받아서 이전 함수의 출력을 기다린 값을 넘겨받아요. +- `functions`: 왼쪽에서 오른쪽으로 합성할 함수들이에요. 첫 번째 함수는 인자를 몇 개든 받을 수 있고, 나머지는 모두 인자를 하나 받아서 이전 함수의 출력을 기다린 값을 넘겨받아요. #### 반환 값 diff --git a/docs/zh_hans/fp/reference/flowAsync.md b/docs/zh_hans/fp/reference/flowAsync.md index 74a35b750..9b5fb6e39 100644 --- a/docs/zh_hans/fp/reference/flowAsync.md +++ b/docs/zh_hans/fp/reference/flowAsync.md @@ -59,7 +59,7 @@ await risky(1); // 以 Error: user 1 not found 拒绝 #### 参数 -- `functions` (`Array<(...args: any[]) => any>`): 从左到右组合的函数。第一个函数可以接收任意数量的参数,其余函数都是一元的,接收上一个函数输出被等待后的值。 +- `functions`: 从左到右组合的函数。第一个函数可以接收任意数量的参数,其余函数都是一元的,接收上一个函数输出被等待后的值。 #### 返回值 From 8e4057542a0d82e024c09a689a7453099dc0c541 Mon Sep 17 00:00:00 2001 From: Sojin Park Date: Sat, 22 Aug 2026 17:38:09 +0900 Subject: [PATCH 4/4] docs(fp): remove type annotations from flowAsync return docs --- docs/fp/reference/flowAsync.md | 2 +- docs/ja/fp/reference/flowAsync.md | 2 +- docs/ko/fp/reference/flowAsync.md | 2 +- docs/zh_hans/fp/reference/flowAsync.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/fp/reference/flowAsync.md b/docs/fp/reference/flowAsync.md index a442e667c..cc93a5079 100644 --- a/docs/fp/reference/flowAsync.md +++ b/docs/fp/reference/flowAsync.md @@ -63,4 +63,4 @@ await risky(1); // rejects with Error: user 1 not found #### Returns -(`(...args: any[]) => Promise`): 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. +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 index 884cc35f5..0c67886d5 100644 --- a/docs/ja/fp/reference/flowAsync.md +++ b/docs/ja/fp/reference/flowAsync.md @@ -63,4 +63,4 @@ await risky(1); // Error: user 1 not found で拒否されます。 #### 戻り値 -(`(...args: any[]) => Promise`): すべての関数を順番に適用し、各結果を待機する新しい非同期関数です。最初の関数と同じパラメータを受け取り、最後の関数の結果を待機した値で解決されます。公開されているオーバーロードは、チェーンから正確な型を推論します。 +すべての関数を順番に適用し、各結果を待機する新しい非同期関数です。最初の関数と同じパラメータを受け取り、最後の関数の結果を待機した値で解決されます。公開されているオーバーロードは、チェーンから正確な型を推論します。 diff --git a/docs/ko/fp/reference/flowAsync.md b/docs/ko/fp/reference/flowAsync.md index 24f051073..9fe6f2099 100644 --- a/docs/ko/fp/reference/flowAsync.md +++ b/docs/ko/fp/reference/flowAsync.md @@ -63,4 +63,4 @@ await risky(1); // Error: user 1 not found 로 거부돼요. #### 반환 값 -(`(...args: any[]) => Promise`): 모든 함수를 순서대로 적용하면서 각 결과를 기다리는 새로운 비동기 함수예요. 첫 번째 함수와 같은 파라미터를 받아서 마지막 함수의 결과를 기다린 값으로 해결돼요. 공개된 오버로드는 체인에서 정확한 타입을 추론해요. +모든 함수를 순서대로 적용하면서 각 결과를 기다리는 새로운 비동기 함수예요. 첫 번째 함수와 같은 파라미터를 받아서 마지막 함수의 결과를 기다린 값으로 해결돼요. 공개된 오버로드는 체인에서 정확한 타입을 추론해요. diff --git a/docs/zh_hans/fp/reference/flowAsync.md b/docs/zh_hans/fp/reference/flowAsync.md index 9b5fb6e39..aa5abdc59 100644 --- a/docs/zh_hans/fp/reference/flowAsync.md +++ b/docs/zh_hans/fp/reference/flowAsync.md @@ -63,4 +63,4 @@ await risky(1); // 以 Error: user 1 not found 拒绝 #### 返回值 -(`(...args: any[]) => Promise`): 一个依次应用所有函数并等待每个结果的新异步函数。它接收与第一个函数相同的参数,并解决为最后一个函数结果被等待后的值。公开的重载会从整条链中推断出精确的类型。 +一个依次应用所有函数并等待每个结果的新异步函数。它接收与第一个函数相同的参数,并解决为最后一个函数结果被等待后的值。公开的重载会从整条链中推断出精确的类型。