From 52a09c35e608c6bd2a12607585467cfc21ad4037 Mon Sep 17 00:00:00 2001 From: ehsanKey Date: Fri, 21 Nov 2025 15:13:15 +0330 Subject: [PATCH 1/5] feat(pipe): implement pipe function with documentation and tests --- docs/pages/function/pipe.mdx | 43 ++++++++++++++++++++++++++++++++++++ src/function/pipe.spec.ts | 32 +++++++++++++++++++++++++++ src/function/pipe.ts | 19 ++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 docs/pages/function/pipe.mdx create mode 100644 src/function/pipe.spec.ts create mode 100644 src/function/pipe.ts diff --git a/docs/pages/function/pipe.mdx b/docs/pages/function/pipe.mdx new file mode 100644 index 00000000..40a7fd77 --- /dev/null +++ b/docs/pages/function/pipe.mdx @@ -0,0 +1,43 @@ +# pipe + +Composes multiple functions from left to right. +Each function receives the output of the previous one. + +### Import + +```typescript copy +import { pipe } from '@fullstacksjs/toolbox'; +```` + +### Signature + +```typescript copy +function pipe(...fns: ((arg: T) => T)[]): (initial: T) => T +``` + +### Examples + +```typescript copy +// Compose number functions +const add = (x: number) => x + 1; +const multiply = (x: number) => x * 2; + +const addThenMultiply = pipe(add, multiply); +console.log(addThenMultiply(5)); // 12 + +// Single function +const square = (x: number) => x * x; +const squarePipe = pipe(square); +console.log(squarePipe(4)); // 16 + +// String functions +const trim = (s: string) => s.trim(); +const toUpper = (s: string) => s.toUpperCase(); + +const normalize = pipe(trim, toUpper); +console.log(normalize(' hello ')); // "HELLO" + +// Identity (no functions) +const identity = pipe(); +console.log(identity(42)); // 42 +``` \ No newline at end of file diff --git a/src/function/pipe.spec.ts b/src/function/pipe.spec.ts new file mode 100644 index 00000000..bef158eb --- /dev/null +++ b/src/function/pipe.spec.ts @@ -0,0 +1,32 @@ +import { pipe } from './pipe'; + +describe('pipe', () => { + it('should compose functions from left to right', () => { + const add = (x: number) => x + 1; + const multiply = (x: number) => x * 2; + const addThenMultiply = pipe(add, multiply); + + expect(addThenMultiply(5)).toBe(12); + }); + + it('should handle single function', () => { + const square = (x: number) => x * x; + const squarePipe = pipe(square); + + expect(squarePipe(4)).toBe(16); + }); + + it('should handle no functions', () => { + const identity = pipe(); + + expect(identity(7)).toBe(7); + }); + + it('should work with same-type functions only', () => { + const trim = (s: string) => s.trim(); + const toUpper = (s: string) => s.toUpperCase(); + const normalize = pipe(trim, toUpper); + + expect(normalize(' hey ')).toBe('HEY'); + }); +}); diff --git a/src/function/pipe.ts b/src/function/pipe.ts new file mode 100644 index 00000000..5e02cb11 --- /dev/null +++ b/src/function/pipe.ts @@ -0,0 +1,19 @@ +/** + * Creates a left-to-right function composition. + * + * Each function receives the output of the previous one. + * + * @param fns - A list of functions to compose from left to right. + * @returns A new function that takes an initial value and runs it through all functions. + * + * @example + * const add = (x: number) => x + 1; + * const multiply = (x: number) => x * 2; + * + * const addThenMultiply = pipe(add, multiply); + * + * console.log(addThenMultiply(5)); // Output: 12 + */ +export function pipe(...fns: ((arg: T) => T)[]) { + return (initial: T): T => fns.reduce((value, fn) => fn(value), initial); +} From a6c3bbf44efbbc09e7deaced982b6a35f0ef3c27 Mon Sep 17 00:00:00 2001 From: ehsanKey Date: Thu, 27 Nov 2025 01:08:39 +0330 Subject: [PATCH 2/5] feat(pipe): implement pipe function with type-safe overloads --- docs/pages/function/pipe.mdx | 35 ++++++--------------- src/function/pipe.spec.ts | 44 +++++++++++++++++++++----- src/function/pipe.ts | 60 +++++++++++++++++++++++++----------- 3 files changed, 89 insertions(+), 50 deletions(-) diff --git a/docs/pages/function/pipe.mdx b/docs/pages/function/pipe.mdx index 40a7fd77..dc13c2e1 100644 --- a/docs/pages/function/pipe.mdx +++ b/docs/pages/function/pipe.mdx @@ -1,43 +1,28 @@ # pipe -Composes multiple functions from left to right. -Each function receives the output of the previous one. +Composes multiple functions from left to right. Each function receives the output of the previous one. ### Import ```typescript copy import { pipe } from '@fullstacksjs/toolbox'; -```` +``` ### Signature ```typescript copy -function pipe(...fns: ((arg: T) => T)[]): (initial: T) => T +function pipe( + ...fns: Function[] +): (...args: A) => B; ``` ### Examples ```typescript copy -// Compose number functions -const add = (x: number) => x + 1; -const multiply = (x: number) => x * 2; - -const addThenMultiply = pipe(add, multiply); -console.log(addThenMultiply(5)); // 12 - -// Single function -const square = (x: number) => x * x; -const squarePipe = pipe(square); -console.log(squarePipe(4)); // 16 - -// String functions -const trim = (s: string) => s.trim(); -const toUpper = (s: string) => s.toUpperCase(); - -const normalize = pipe(trim, toUpper); -console.log(normalize(' hello ')); // "HELLO" +const add = (x: number, y: number) => x + y; +const double = (x: number) => x * 2; +const toString = (x: number) => x.toString(); -// Identity (no functions) -const identity = pipe(); -console.log(identity(42)); // 42 +const calculate = pipe(add, double, toString); +console.log(calculate(3, 4)); // ((3 + 4) * 2).toString() => "14" ``` \ No newline at end of file diff --git a/src/function/pipe.spec.ts b/src/function/pipe.spec.ts index bef158eb..579d37a8 100644 --- a/src/function/pipe.spec.ts +++ b/src/function/pipe.spec.ts @@ -16,17 +16,47 @@ describe('pipe', () => { expect(squarePipe(4)).toBe(16); }); - it('should handle no functions', () => { - const identity = pipe(); - - expect(identity(7)).toBe(7); - }); - - it('should work with same-type functions only', () => { + it('should work with same-type functions', () => { const trim = (s: string) => s.trim(); const toUpper = (s: string) => s.toUpperCase(); const normalize = pipe(trim, toUpper); expect(normalize(' hey ')).toBe('HEY'); }); + + it('should work with different types', () => { + const toNumber = (s: string) => Number.parseInt(s, 10); + const double = (n: number) => n * 2; + const toString = (n: number) => n.toString(); + const transform = pipe(toNumber, double, toString); + + expect(transform('5')).toBe('10'); + }); + + it('should handle multiple arguments in first function', () => { + const add = (a: number, b: number) => a + b; + const square = (x: number) => x * x; + const addThenSquare = pipe(add, square); + + expect(addThenSquare(3, 4)).toBe(49); // (3 + 4) ^ 2 = 49 + }); + + it('should compose 3+ functions', () => { + const add1 = (x: number) => x + 1; + const multiply2 = (x: number) => x * 2; + const subtract3 = (x: number) => x - 3; + const composed = pipe(add1, multiply2, subtract3); + + expect(composed(5)).toBe(9); // ((5 + 1) * 2) - 3 = 9 + }); + + it('should preserve type safety across transformations', () => { + const split = (s: string) => s.split(','); + const count = (arr: string[]) => arr.length; + const isEven = (n: number) => n % 2 === 0; + const check = pipe(split, count, isEven); + + expect(check('a,b,c')).toBe(false); + expect(check('a,b,c,d')).toBe(true); + }); }); diff --git a/src/function/pipe.ts b/src/function/pipe.ts index 5e02cb11..a763fb36 100644 --- a/src/function/pipe.ts +++ b/src/function/pipe.ts @@ -1,19 +1,43 @@ -/** - * Creates a left-to-right function composition. - * - * Each function receives the output of the previous one. - * - * @param fns - A list of functions to compose from left to right. - * @returns A new function that takes an initial value and runs it through all functions. - * - * @example - * const add = (x: number) => x + 1; - * const multiply = (x: number) => x * 2; - * - * const addThenMultiply = pipe(add, multiply); - * - * console.log(addThenMultiply(5)); // Output: 12 - */ -export function pipe(...fns: ((arg: T) => T)[]) { - return (initial: T): T => fns.reduce((value, fn) => fn(value), initial); +type UnknownFunction = (...args: unknown[]) => unknown; + +export function pipe( + ab: (...a: A) => B, +): (...a: A) => B; +export function pipe( + ab: (...a: A) => B, + bc: (b: B) => C, +): (...a: A) => C; +export function pipe( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, +): (...a: A) => D; +export function pipe( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, +): (...a: A) => E; +export function pipe( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, +): (...a: A) => F; +export function pipe( + ab: (...a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, +): (...a: A) => G; + +export function pipe(...fns: UnknownFunction[]): UnknownFunction { + return fns.reduce( + (acc, fn) => + (...args: unknown[]) => + fn(acc(...args)), + ); } From 56bfe2c2d6026a3d3ff87240039e3cf0c840b2f2 Mon Sep 17 00:00:00 2001 From: ehsanKey Date: Thu, 27 Nov 2025 16:10:45 +0330 Subject: [PATCH 3/5] refactor: use value-first reduce instead of curried functions --- docs/pages/function/pipe.mdx | 20 +++++++++------ src/function/pipe.spec.ts | 37 +++++++++++----------------- src/function/pipe.ts | 47 ++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 56 deletions(-) diff --git a/docs/pages/function/pipe.mdx b/docs/pages/function/pipe.mdx index dc13c2e1..9a9b19be 100644 --- a/docs/pages/function/pipe.mdx +++ b/docs/pages/function/pipe.mdx @@ -1,6 +1,6 @@ # pipe -Composes multiple functions from left to right. Each function receives the output of the previous one. +Pipes a value through multiple functions from left to right. Each function receives the output of the previous one. ### Import @@ -11,18 +11,24 @@ import { pipe } from '@fullstacksjs/toolbox'; ### Signature ```typescript copy -function pipe( - ...fns: Function[] -): (...args: A) => B; +function pipe(value: A, ...fns: Function[]): B; ``` ### Examples ```typescript copy -const add = (x: number, y: number) => x + y; +const add = (x: number) => x + 1; const double = (x: number) => x * 2; const toString = (x: number) => x.toString(); -const calculate = pipe(add, double, toString); -console.log(calculate(3, 4)); // ((3 + 4) * 2).toString() => "14" +const result = pipe(5, add, double, toString); +console.log(result); // ((5 + 1) * 2).toString() => "12" + + +const split = (str: string) => str.split(' '); +const getLength = (arr: string[]) => arr.length; +const isEven = (num: number) => num % 2 === 0; + +const hasEvenWordCount = pipe('hello world from typescript', split, getLength, isEven); +console.log(hasEvenWordCount); // true (4 words) ``` \ No newline at end of file diff --git a/src/function/pipe.spec.ts b/src/function/pipe.spec.ts index 579d37a8..24edfbfe 100644 --- a/src/function/pipe.spec.ts +++ b/src/function/pipe.spec.ts @@ -1,62 +1,53 @@ import { pipe } from './pipe'; describe('pipe', () => { - it('should compose functions from left to right', () => { + it('should pipe value through functions from left to right', () => { const add = (x: number) => x + 1; const multiply = (x: number) => x * 2; - const addThenMultiply = pipe(add, multiply); + const result = pipe(5, add, multiply); - expect(addThenMultiply(5)).toBe(12); + expect(result).toBe(12); }); it('should handle single function', () => { const square = (x: number) => x * x; - const squarePipe = pipe(square); + const result = pipe(4, square); - expect(squarePipe(4)).toBe(16); + expect(result).toBe(16); }); it('should work with same-type functions', () => { const trim = (s: string) => s.trim(); const toUpper = (s: string) => s.toUpperCase(); - const normalize = pipe(trim, toUpper); + const result = pipe(' hey ', trim, toUpper); - expect(normalize(' hey ')).toBe('HEY'); + expect(result).toBe('HEY'); }); it('should work with different types', () => { const toNumber = (s: string) => Number.parseInt(s, 10); const double = (n: number) => n * 2; const toString = (n: number) => n.toString(); - const transform = pipe(toNumber, double, toString); + const result = pipe('5', toNumber, double, toString); - expect(transform('5')).toBe('10'); + expect(result).toBe('10'); }); - it('should handle multiple arguments in first function', () => { - const add = (a: number, b: number) => a + b; - const square = (x: number) => x * x; - const addThenSquare = pipe(add, square); - - expect(addThenSquare(3, 4)).toBe(49); // (3 + 4) ^ 2 = 49 - }); - - it('should compose 3+ functions', () => { + it('should pipe through 3+ functions', () => { const add1 = (x: number) => x + 1; const multiply2 = (x: number) => x * 2; const subtract3 = (x: number) => x - 3; - const composed = pipe(add1, multiply2, subtract3); + const result = pipe(5, add1, multiply2, subtract3); - expect(composed(5)).toBe(9); // ((5 + 1) * 2) - 3 = 9 + expect(result).toBe(9); // ((5 + 1) * 2) - 3 = 9 }); it('should preserve type safety across transformations', () => { const split = (s: string) => s.split(','); const count = (arr: string[]) => arr.length; const isEven = (n: number) => n % 2 === 0; - const check = pipe(split, count, isEven); - expect(check('a,b,c')).toBe(false); - expect(check('a,b,c,d')).toBe(true); + expect(pipe('a,b,c', split, count, isEven)).toBe(false); + expect(pipe('a,b,c,d', split, count, isEven)).toBe(true); }); }); diff --git a/src/function/pipe.ts b/src/function/pipe.ts index a763fb36..a5f0d944 100644 --- a/src/function/pipe.ts +++ b/src/function/pipe.ts @@ -1,43 +1,38 @@ -type UnknownFunction = (...args: unknown[]) => unknown; +type UnknownFunction = (arg: unknown) => unknown; -export function pipe( - ab: (...a: A) => B, -): (...a: A) => B; -export function pipe( - ab: (...a: A) => B, - bc: (b: B) => C, -): (...a: A) => C; -export function pipe( - ab: (...a: A) => B, +export function pipe(value: A, ab: (a: A) => B): B; +export function pipe(value: A, ab: (a: A) => B, bc: (b: B) => C): C; +export function pipe( + value: A, + ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, -): (...a: A) => D; -export function pipe( - ab: (...a: A) => B, +): D; +export function pipe( + value: A, + ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, -): (...a: A) => E; -export function pipe( - ab: (...a: A) => B, +): E; +export function pipe( + value: A, + ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, -): (...a: A) => F; -export function pipe( - ab: (...a: A) => B, +): F; +export function pipe( + value: A, + ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, -): (...a: A) => G; +): G; -export function pipe(...fns: UnknownFunction[]): UnknownFunction { - return fns.reduce( - (acc, fn) => - (...args: unknown[]) => - fn(acc(...args)), - ); +export function pipe(value: unknown, ...fns: UnknownFunction[]): unknown { + return fns.reduce((acc, fn) => fn(acc), value); } From 9207acf61832ce1fb0fe35ab8098d87093d4a80f Mon Sep 17 00:00:00 2001 From: ehsanKey Date: Thu, 27 Nov 2025 22:19:40 +0330 Subject: [PATCH 4/5] feat(pipe): add jsdoc --- src/function/pipe.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/function/pipe.ts b/src/function/pipe.ts index a5f0d944..644cf0c3 100644 --- a/src/function/pipe.ts +++ b/src/function/pipe.ts @@ -1,5 +1,33 @@ type UnknownFunction = (arg: unknown) => unknown; +/** + * Pipes a value through a series of functions, applying each function to the result of the previous one. + * The value flows from left to right through the function pipeline. + * + * @template A - The type of the initial value + * @template B - The type after the first transformation + * @param {A} value - The initial value to pipe through the functions + * @param {...Function} fns - The functions to apply in sequence + * @returns {B} The final transformed value + * + * @example + * + * const add = (x: number) => x + 1; + * const double = (x: number) => x * 2; + * const toString = (x: number) => x.toString(); + * + * const result = pipe(5, add, double, toString); + * console.log(result); // ((5 + 1) * 2).toString() => "12" + * + * @example + * + * const split = (str: string) => str.split(' '); + * const getLength = (arr: string[]) => arr.length; + * const isEven = (num: number) => num % 2 === 0; + * + * const hasEvenWordCount = pipe('hello world from typescript', split, getLength, isEven); + * console.log(hasEvenWordCount); // true (4 words) + */ export function pipe(value: A, ab: (a: A) => B): B; export function pipe(value: A, ab: (a: A) => B, bc: (b: B) => C): C; export function pipe( From 48dde4a00ceb6c9b2570d2db9c1869afbeeb9997 Mon Sep 17 00:00:00 2001 From: ehsanKey Date: Fri, 28 Nov 2025 13:27:00 +0330 Subject: [PATCH 5/5] fix(pipe): export pipe function from index --- src/function/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/function/index.ts b/src/function/index.ts index 6d6ad372..9288199e 100644 --- a/src/function/index.ts +++ b/src/function/index.ts @@ -4,6 +4,7 @@ export { debounce } from './debounce.ts'; export { formatSeconds } from './formatSeconds.ts'; export { noop } from './noop.ts'; export { not } from './not.ts'; +export { pipe } from './pipe.ts'; export { sleep } from './sleep.ts'; export { throttle } from './throttle.ts'; export { tryOr } from './tryOr.ts';