fp-pack 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +27 -7
  2. package/dist/fp-pack.umd.js +1 -1
  3. package/dist/fp-pack.umd.js.map +1 -1
  4. package/dist/implement/async/index.d.ts +1 -0
  5. package/dist/implement/async/index.d.ts.map +1 -1
  6. package/dist/implement/async/pipeAsyncSideEffectStrict.d.ts +57 -0
  7. package/dist/implement/async/pipeAsyncSideEffectStrict.d.ts.map +1 -0
  8. package/dist/implement/async/pipeAsyncSideEffectStrict.mjs +16 -0
  9. package/dist/implement/async/pipeAsyncSideEffectStrict.mjs.map +1 -0
  10. package/dist/implement/composition/from.d.ts +6 -0
  11. package/dist/implement/composition/from.d.ts.map +1 -0
  12. package/dist/implement/composition/from.mjs +7 -0
  13. package/dist/implement/composition/from.mjs.map +1 -0
  14. package/dist/implement/composition/index.d.ts +2 -0
  15. package/dist/implement/composition/index.d.ts.map +1 -1
  16. package/dist/implement/composition/pipe.d.ts +6 -0
  17. package/dist/implement/composition/pipe.d.ts.map +1 -1
  18. package/dist/implement/composition/pipe.mjs.map +1 -1
  19. package/dist/implement/composition/pipe.type-test.d.ts +29 -0
  20. package/dist/implement/composition/pipe.type-test.d.ts.map +1 -1
  21. package/dist/implement/composition/pipeSideEffect.d.ts +6 -0
  22. package/dist/implement/composition/pipeSideEffect.d.ts.map +1 -1
  23. package/dist/implement/composition/pipeSideEffect.mjs.map +1 -1
  24. package/dist/implement/composition/pipeSideEffectStrict.d.ts +53 -0
  25. package/dist/implement/composition/pipeSideEffectStrict.d.ts.map +1 -0
  26. package/dist/implement/composition/pipeSideEffectStrict.mjs +16 -0
  27. package/dist/implement/composition/pipeSideEffectStrict.mjs.map +1 -0
  28. package/dist/implement/control/ifElse.d.ts +4 -3
  29. package/dist/implement/control/ifElse.d.ts.map +1 -1
  30. package/dist/implement/control/ifElse.mjs.map +1 -1
  31. package/dist/implement/control/unless.d.ts +14 -2
  32. package/dist/implement/control/unless.d.ts.map +1 -1
  33. package/dist/implement/control/unless.mjs.map +1 -1
  34. package/dist/implement/control/when.d.ts +14 -2
  35. package/dist/implement/control/when.d.ts.map +1 -1
  36. package/dist/implement/control/when.mjs.map +1 -1
  37. package/dist/implement/object/index.d.ts +1 -0
  38. package/dist/implement/object/index.d.ts.map +1 -1
  39. package/dist/implement/object/propStrict.d.ts +7 -0
  40. package/dist/implement/object/propStrict.d.ts.map +1 -0
  41. package/dist/implement/object/propStrict.mjs +12 -0
  42. package/dist/implement/object/propStrict.mjs.map +1 -0
  43. package/dist/index.mjs +248 -240
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/skills/fp-pack/SKILL.md +1699 -0
  46. package/dist/skills/fp-pack.md +56 -8
  47. package/package.json +1 -1
  48. package/src/implement/async/index.ts +1 -0
  49. package/src/implement/async/pipeAsyncSideEffectStrict.test.ts +23 -0
  50. package/src/implement/async/pipeAsyncSideEffectStrict.ts +140 -0
  51. package/src/implement/composition/from.test.ts +16 -0
  52. package/src/implement/composition/from.ts +8 -0
  53. package/src/implement/composition/index.ts +2 -0
  54. package/src/implement/composition/pipe.test.ts +8 -0
  55. package/src/implement/composition/pipe.ts +12 -0
  56. package/src/implement/composition/pipe.type-test.ts +49 -0
  57. package/src/implement/composition/pipeSideEffect.test.ts +8 -0
  58. package/src/implement/composition/pipeSideEffect.ts +17 -0
  59. package/src/implement/composition/pipeSideEffectStrict.test.ts +23 -0
  60. package/src/implement/composition/pipeSideEffectStrict.ts +134 -0
  61. package/src/implement/control/curried.test.ts +18 -0
  62. package/src/implement/control/ifElse.ts +5 -3
  63. package/src/implement/control/unless.ts +20 -2
  64. package/src/implement/control/when.ts +20 -2
  65. package/src/implement/object/index.ts +1 -0
  66. package/src/implement/object/propStrict.test.ts +29 -0
  67. package/src/implement/object/propStrict.ts +20 -0
@@ -1,6 +1,6 @@
1
1
  # fp-pack AI Agent Skills
2
2
 
3
- Document Version: 0.1.0
3
+ Document Version: 0.2.0
4
4
 
5
5
  This document provides guidelines for AI coding assistants when working in projects that use fp-pack. Follow these instructions to write clean, declarative, functional code using fp-pack's utilities.
6
6
 
@@ -11,7 +11,7 @@ fp-pack is a TypeScript functional programming library focused on:
11
11
  1. **Function Composition**: Use `pipe` and `pipeAsync` as the primary tools for combining operations
12
12
  2. **Declarative Code**: Prefer function composition over imperative loops and mutations
13
13
  3. **No Monad Pattern**: Traditional FP monads (Option, Either, etc.) are NOT used - they don't compose well with `pipe`
14
- 4. **SideEffect Pattern**: Handle errors and side effects using `SideEffect` with `pipeSideEffect` / `pipeAsyncSideEffect` pipelines
14
+ 4. **SideEffect Pattern**: Handle errors and side effects using `SideEffect` with `pipeSideEffect` / `pipeAsyncSideEffect` pipelines (use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict` for strict unions)
15
15
  5. **Lazy Evaluation**: Use `stream/*` functions for efficient iterable processing
16
16
 
17
17
  ## Core Composition Functions
@@ -43,7 +43,7 @@ const processUsers = (users: User[]) => {
43
43
  };
44
44
  ```
45
45
 
46
- > For SideEffect-based early exits, use `pipeSideEffect`.
46
+ > For SideEffect-based early exits, use `pipeSideEffect` (or `pipeSideEffectStrict` when you want strict unions).
47
47
 
48
48
  ### pipeAsync - Asynchronous Function Composition
49
49
 
@@ -67,7 +67,7 @@ const fetchUserData = async (userId: string) => {
67
67
  };
68
68
  ```
69
69
 
70
- > For SideEffect-aware async pipelines, use `pipeAsyncSideEffect`.
70
+ > For SideEffect-aware async pipelines, use `pipeAsyncSideEffect` (or `pipeAsyncSideEffectStrict` for strict unions).
71
71
 
72
72
  ## SideEffect Pattern - For Special Cases Only
73
73
 
@@ -79,6 +79,7 @@ const fetchUserData = async (userId: string) => {
79
79
  - Optional chaining patterns
80
80
 
81
81
  For regular error handling, standard try-catch or error propagation is perfectly fine.
82
+ If you want precise SideEffect unions across branches, use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`.
82
83
 
83
84
  ```typescript
84
85
  // MOST CASES: Just use pipe with regular error handling
@@ -201,6 +202,7 @@ const result = Array.from({ length: 1000000 }, (_, i) => i + 1)
201
202
  ### Composition
202
203
  - `pipe` - Left-to-right function composition (sync)
203
204
  - `pipeSideEffect` - Left-to-right composition with SideEffect short-circuiting
205
+ - `pipeSideEffectStrict` - SideEffect composition with strict effect unions
204
206
  - `compose` - Right-to-left function composition
205
207
  - `curry` - Curry a function
206
208
  - `partial` - Partial application
@@ -208,6 +210,7 @@ const result = Array.from({ length: 1000000 }, (_, i) => i + 1)
208
210
  - `complement` - Logical negation
209
211
  - `identity` - Return input unchanged
210
212
  - `constant` - Always return the same value
213
+ - `from` - Ignore input and return a fixed value
211
214
  - `tap` - Execute side effect and return original value
212
215
  - `once` - Execute function only once
213
216
  - `memoize` - Cache function results
@@ -219,6 +222,7 @@ const result = Array.from({ length: 1000000 }, (_, i) => i + 1)
219
222
  ### Async
220
223
  - `pipeAsync` - Async function composition
221
224
  - `pipeAsyncSideEffect` - Async composition with SideEffect short-circuiting
225
+ - `pipeAsyncSideEffectStrict` - Async SideEffect composition with strict effect unions
222
226
  - `delay` - Delay execution
223
227
  - `timeout` - Add timeout to promise
224
228
  - `retry` - Retry failed operations
@@ -386,8 +390,10 @@ Most data transformations are pure and don't need SideEffect handling. Use `pipe
386
390
  - **`pipeAsync`** - Async, **pure** transformations (99% of cases)
387
391
  - **`pipeSideEffect`** - **Only when you need** SideEffect short-circuiting (sync)
388
392
  - **`pipeAsyncSideEffect`** - **Only when you need** SideEffect short-circuiting (async)
393
+ - **`pipeSideEffectStrict`** - Sync SideEffect pipelines with strict effect unions
394
+ - **`pipeAsyncSideEffectStrict`** - Async SideEffect pipelines with strict effect unions
389
395
 
390
- **Important:** `pipe` and `pipeAsync` are for **pure** functions only—they don't handle `SideEffect`. If your pipeline can return `SideEffect`, use `pipeSideEffect` or `pipeAsyncSideEffect` instead.
396
+ **Important:** `pipe` and `pipeAsync` are for **pure** functions only—they don't handle `SideEffect`. If your pipeline can return `SideEffect`, use `pipeSideEffect` or `pipeAsyncSideEffect` instead. Choose the strict variants when you need precise unions for SideEffect results.
391
397
 
392
398
  ```typescript
393
399
  // Sync: use pipe
@@ -408,7 +414,7 @@ const processUsers = pipeAsync(
408
414
 
409
415
  **🔄 Critical Rule: SideEffect Contagion**
410
416
 
411
- Once you use `pipeSideEffect` or `pipeAsyncSideEffect`, the result is **always `T | SideEffect`** (or `Promise<T | SideEffect>` for async).
417
+ Once you use `pipeSideEffect` or `pipeAsyncSideEffect`, the result is **always `T | SideEffect`** (or `Promise<T | SideEffect>` for async). The same rule applies to strict variants.
412
418
 
413
419
  If you want to continue composing this result, you **MUST** keep using SideEffect-aware pipes. You **CANNOT** switch back to `pipe` or `pipeAsync` because they don't handle `SideEffect`.
414
420
 
@@ -510,6 +516,47 @@ const gradeToLetter = cond([
510
516
  ]);
511
517
  ```
512
518
 
519
+ ### 6.1 Type-Safety Tips (prop/ifElse/cond)
520
+
521
+ - `prop` returns `T[K] | undefined`. Use `propOr` (or guard) before array operations.
522
+ - `ifElse` expects **functions** for both branches. If you already have a value, wrap it: `() => value`.
523
+ - Use `from(value)` when you need a unary function that ignores input (handy for `ifElse` branches).
524
+ - `cond` returns `R | undefined`. Add a default branch and coalesce when you need a strict result.
525
+ - In `pipeSideEffect`, keep step return types aligned to avoid wide unions.
526
+
527
+ ```typescript
528
+ import { pipe, propOr, append, assoc, ifElse, cond, from } from 'fp-pack';
529
+
530
+ // propOr keeps the type strict for array ops
531
+ const addTodo = (text: string, state: AppState) =>
532
+ pipe(
533
+ propOr([], 'todos'),
534
+ append(createTodo(text)),
535
+ (todos) => assoc('todos', todos, state)
536
+ )(state);
537
+
538
+ // ifElse expects functions, not values
539
+ const toggleTodo = (id: string) => ifElse(
540
+ (todo: Todo) => todo.id === id,
541
+ assoc('completed', true),
542
+ (todo) => todo
543
+ );
544
+
545
+ // from is useful for constant branches
546
+ const statusLabel = ifElse(
547
+ (score: number) => score >= 60,
548
+ from('pass'),
549
+ from('fail')
550
+ );
551
+
552
+ // cond still returns R | undefined, so coalesce if needed
553
+ const grade = (score: number) =>
554
+ cond([
555
+ [(n: number) => n >= 90, () => 'A'],
556
+ [() => true, () => 'F']
557
+ ])(score) ?? 'F';
558
+ ```
559
+
513
560
  ### 7. Object Transformations
514
561
 
515
562
  ```typescript
@@ -610,18 +657,19 @@ const updateUser = assoc('lastLogin', new Date());
610
657
  ### Import Paths
611
658
  - Main functions: `import { pipe, map, filter } from 'fp-pack'`
612
659
  - Async: `import { pipeAsync, delay, retry } from 'fp-pack'`
613
- - SideEffect: `import { pipeSideEffect, pipeAsyncSideEffect, SideEffect } from 'fp-pack'`
660
+ - SideEffect: `import { pipeSideEffect, pipeSideEffectStrict, pipeAsyncSideEffect, pipeAsyncSideEffectStrict, SideEffect } from 'fp-pack'`
614
661
  - Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
615
662
 
616
663
  ### When to Use What
617
664
  - **Pure sync transformations**: `pipe` + array/object functions
618
665
  - **Pure async operations**: `pipeAsync`
619
666
  - **Error handling with SideEffect**: `pipeSideEffect` (sync) / `pipeAsyncSideEffect` (async)
667
+ - **Strict SideEffect unions**: `pipeSideEffectStrict` (sync) / `pipeAsyncSideEffectStrict` (async)
620
668
  - **Type-safe result handling**: `isSideEffect` for precise type narrowing (⚠️ **ALWAYS prefer this over bare `runPipeResult`** to avoid `any` types)
621
669
  - **Execute SideEffect**: `runPipeResult` (call OUTSIDE pipelines). **⚠️ CRITICAL:** Returns `any` type without type narrowing due to default `R=any` parameter
622
670
  - **Large datasets**: `stream/*` functions
623
671
  - **Conditionals**: `ifElse`, `when`, `unless`, `cond`
624
- - **Object access**: `prop`, `path`, `pick`, `omit`
672
+ - **Object access**: `prop`, `propStrict`, `path`, `pick`, `omit`
625
673
  - **Object updates**: `assoc`, `merge`, `evolve`
626
674
 
627
675
  ## UI Framework Integration Patterns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fp-pack",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Functional programming utilities library for TypeScript",
6
6
  "keywords": [
@@ -1,5 +1,6 @@
1
1
  export { default as pipeAsync } from './pipeAsync';
2
2
  export { default as pipeAsyncSideEffect } from './pipeAsyncSideEffect';
3
+ export { default as pipeAsyncSideEffectStrict } from './pipeAsyncSideEffectStrict';
3
4
  export { default as delay } from './delay';
4
5
  export { default as timeout } from './timeout';
5
6
  export { default as retry } from './retry';
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import pipeAsyncSideEffectStrict from './pipeAsyncSideEffectStrict';
3
+ import SideEffect from '../composition/sideEffect';
4
+
5
+ describe('pipeAsyncSideEffectStrict', () => {
6
+ it('short-circuits when SideEffect is returned', async () => {
7
+ const effect = new SideEffect(() => 'effect');
8
+ const stop = async (_value: number) => effect;
9
+ const after = async (_value: number) => _value + 1;
10
+
11
+ const fn = pipeAsyncSideEffectStrict(async (n: number) => n + 1, stop, after);
12
+ const result = await fn(1);
13
+
14
+ expect(result).toBe(effect);
15
+ });
16
+
17
+ it('returns SideEffect inputs without executing the pipeline', async () => {
18
+ const effect = new SideEffect(() => 'effect');
19
+ const fn = pipeAsyncSideEffectStrict(async (n: number) => n + 1);
20
+
21
+ await expect(fn(effect)).resolves.toBe(effect);
22
+ });
23
+ });
@@ -0,0 +1,140 @@
1
+ import SideEffect, { isSideEffect } from '../composition/sideEffect';
2
+
3
+ type AnyFn = (...args: any[]) => any;
4
+ type MaybeSideEffect<T, E> = T | SideEffect<E>;
5
+ type NonSideEffect<T> = Exclude<T, SideEffect<any>>;
6
+ type AsyncOrSync<A, R> = (a: A) => R | Promise<R>;
7
+ type ZeroFn<R> = () => R | Promise<R>;
8
+
9
+ type EffectOfReturn<R> = R extends SideEffect<infer E> ? E : never;
10
+ type EffectOfFn<F> = F extends (...args: any[]) => infer R ? EffectOfReturn<Awaited<R>> : never;
11
+ type EffectsOf<Fns extends AnyFn[]> = EffectOfFn<Fns[number]>;
12
+
13
+ type PipeValueOutputUnary<Fns extends AsyncOrSync<any, any>[]> = Fns extends [AsyncOrSync<any, infer R>]
14
+ ? NonSideEffect<Awaited<R>>
15
+ : Fns extends [AsyncOrSync<any, infer R>, ...infer Rest]
16
+ ? Rest extends [AsyncOrSync<NonSideEffect<Awaited<R>>, any>, ...AsyncOrSync<any, any>[]]
17
+ ? PipeValueOutputUnary<Rest>
18
+ : never
19
+ : never;
20
+
21
+ type PipeValueOutputStrict<Fns extends [AnyFn, ...AnyFn[]]> =
22
+ Fns extends [ZeroFn<infer R>]
23
+ ? NonSideEffect<Awaited<R>>
24
+ : Fns extends [ZeroFn<infer R>, ...infer Rest]
25
+ ? Rest extends [AsyncOrSync<NonSideEffect<Awaited<R>>, any>, ...AsyncOrSync<any, any>[]]
26
+ ? PipeValueOutputUnary<Rest>
27
+ : never
28
+ : Fns extends [AsyncOrSync<any, any>, ...AsyncOrSync<any, any>[]]
29
+ ? PipeValueOutputUnary<Fns>
30
+ : never;
31
+
32
+ type PipeInputStrict<Fns extends [AnyFn, ...AnyFn[]]> = Fns extends [ZeroFn<any>, ...AnyFn[]]
33
+ ? never
34
+ : Fns extends [AsyncOrSync<infer A, any>, ...AnyFn[]]
35
+ ? A
36
+ : never;
37
+
38
+ type Resolve<T> = T extends infer R ? R : never;
39
+
40
+ type PipeAsyncSideEffectStrictUnary<Fns extends [AnyFn, ...AnyFn[]]> = {
41
+ (input: PipeInputStrict<Fns>): Promise<Resolve<MaybeSideEffect<PipeValueOutputStrict<Fns>, EffectsOf<Fns>>>>;
42
+ <EIn>(
43
+ input: PipeInputStrict<Fns> | SideEffect<EIn>
44
+ ): Promise<Resolve<MaybeSideEffect<PipeValueOutputStrict<Fns>, EffectsOf<Fns> | EIn>>>;
45
+ };
46
+
47
+ type PipeAsyncSideEffectStrict<Fns extends [AnyFn, ...AnyFn[]]> = Fns extends [ZeroFn<any>, ...AnyFn[]]
48
+ ? () => Promise<Resolve<MaybeSideEffect<PipeValueOutputStrict<Fns>, EffectsOf<Fns>>>>
49
+ : PipeAsyncSideEffectStrictUnary<Fns>;
50
+
51
+ function pipeAsyncSideEffectStrict<R>(ab: ZeroFn<R>): PipeAsyncSideEffectStrict<[ZeroFn<R>]>;
52
+ function pipeAsyncSideEffectStrict<B, R>(
53
+ ab: ZeroFn<B>,
54
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, R>
55
+ ): PipeAsyncSideEffectStrict<[ZeroFn<B>, AsyncOrSync<NonSideEffect<Awaited<B>>, R>]>;
56
+ function pipeAsyncSideEffectStrict<B, C, R>(
57
+ ab: ZeroFn<B>,
58
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
59
+ cd: AsyncOrSync<NonSideEffect<Awaited<C>>, R>
60
+ ): PipeAsyncSideEffectStrict<[ZeroFn<B>, AsyncOrSync<NonSideEffect<Awaited<B>>, C>, AsyncOrSync<NonSideEffect<Awaited<C>>, R>]>;
61
+ function pipeAsyncSideEffectStrict<B, C, D, R>(
62
+ ab: ZeroFn<B>,
63
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
64
+ cd: AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
65
+ de: AsyncOrSync<NonSideEffect<Awaited<D>>, R>
66
+ ): PipeAsyncSideEffectStrict<[
67
+ ZeroFn<B>,
68
+ AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
69
+ AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
70
+ AsyncOrSync<NonSideEffect<Awaited<D>>, R>
71
+ ]>;
72
+ function pipeAsyncSideEffectStrict<B, C, D, E, R>(
73
+ ab: ZeroFn<B>,
74
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
75
+ cd: AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
76
+ de: AsyncOrSync<NonSideEffect<Awaited<D>>, E>,
77
+ ef: AsyncOrSync<NonSideEffect<Awaited<E>>, R>
78
+ ): PipeAsyncSideEffectStrict<[
79
+ ZeroFn<B>,
80
+ AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
81
+ AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
82
+ AsyncOrSync<NonSideEffect<Awaited<D>>, E>,
83
+ AsyncOrSync<NonSideEffect<Awaited<E>>, R>
84
+ ]>;
85
+ function pipeAsyncSideEffectStrict<A, R>(
86
+ ab: AsyncOrSync<A, R>
87
+ ): PipeAsyncSideEffectStrict<[AsyncOrSync<A, R>]>;
88
+ function pipeAsyncSideEffectStrict<A, B, R>(
89
+ ab: AsyncOrSync<A, B>,
90
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, R>
91
+ ): PipeAsyncSideEffectStrict<[AsyncOrSync<A, B>, AsyncOrSync<NonSideEffect<Awaited<B>>, R>]>;
92
+ function pipeAsyncSideEffectStrict<A, B, C, R>(
93
+ ab: AsyncOrSync<A, B>,
94
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
95
+ cd: AsyncOrSync<NonSideEffect<Awaited<C>>, R>
96
+ ): PipeAsyncSideEffectStrict<[
97
+ AsyncOrSync<A, B>,
98
+ AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
99
+ AsyncOrSync<NonSideEffect<Awaited<C>>, R>
100
+ ]>;
101
+ function pipeAsyncSideEffectStrict<A, B, C, D, R>(
102
+ ab: AsyncOrSync<A, B>,
103
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
104
+ cd: AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
105
+ de: AsyncOrSync<NonSideEffect<Awaited<D>>, R>
106
+ ): PipeAsyncSideEffectStrict<[
107
+ AsyncOrSync<A, B>,
108
+ AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
109
+ AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
110
+ AsyncOrSync<NonSideEffect<Awaited<D>>, R>
111
+ ]>;
112
+ function pipeAsyncSideEffectStrict<A, B, C, D, E, R>(
113
+ ab: AsyncOrSync<A, B>,
114
+ bc: AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
115
+ cd: AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
116
+ de: AsyncOrSync<NonSideEffect<Awaited<D>>, E>,
117
+ ef: AsyncOrSync<NonSideEffect<Awaited<E>>, R>
118
+ ): PipeAsyncSideEffectStrict<[
119
+ AsyncOrSync<A, B>,
120
+ AsyncOrSync<NonSideEffect<Awaited<B>>, C>,
121
+ AsyncOrSync<NonSideEffect<Awaited<C>>, D>,
122
+ AsyncOrSync<NonSideEffect<Awaited<D>>, E>,
123
+ AsyncOrSync<NonSideEffect<Awaited<E>>, R>
124
+ ]>;
125
+
126
+ function pipeAsyncSideEffectStrict<Fns extends [AnyFn, ...AnyFn[]]>(...funcs: Fns): PipeAsyncSideEffectStrict<Fns>;
127
+ function pipeAsyncSideEffectStrict(...funcs: Array<AsyncOrSync<any, any>>) {
128
+ return async (value?: any) => {
129
+ let acc = value;
130
+ for (const fn of funcs) {
131
+ if (isSideEffect(acc)) {
132
+ return acc;
133
+ }
134
+ acc = await fn(acc);
135
+ }
136
+ return acc;
137
+ };
138
+ }
139
+
140
+ export default pipeAsyncSideEffectStrict;
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import from from './from';
3
+
4
+ describe('from', () => {
5
+ it('returns the same value regardless of input', () => {
6
+ const getFive = from(5);
7
+ expect(getFive('x')).toBe(5);
8
+ expect(getFive(123)).toBe(5);
9
+ });
10
+
11
+ it('preserves reference identity', () => {
12
+ const obj = { a: 1 };
13
+ const getObj = from(obj);
14
+ expect(getObj(null)).toBe(obj);
15
+ });
16
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * from - 입력을 무시하고 고정 값을 반환
3
+ */
4
+ function from<T>(value: T) {
5
+ return <I>(_input: I) => value;
6
+ }
7
+
8
+ export default from;
@@ -1,5 +1,6 @@
1
1
  export { default as pipe } from './pipe';
2
2
  export { default as pipeSideEffect } from './pipeSideEffect';
3
+ export { default as pipeSideEffectStrict } from './pipeSideEffectStrict';
3
4
  export { default as compose } from './compose';
4
5
  export { default as curry } from './curry';
5
6
  export { default as partial } from './partial';
@@ -7,6 +8,7 @@ export { default as flip } from './flip';
7
8
  export { default as complement } from './complement';
8
9
  export { default as identity } from './identity';
9
10
  export { default as constant } from './constant';
11
+ export { default as from } from './from';
10
12
  export { default as tap } from './tap';
11
13
  export { default as once } from './once';
12
14
  export { default as memoize } from './memoize';
@@ -28,4 +28,12 @@ describe('pipe', () => {
28
28
  expect(fn(5)).toBe(25);
29
29
  });
30
30
 
31
+ it('supports zero-arity starts', () => {
32
+ const start = () => 3;
33
+ const addOne = (n: number) => n + 1;
34
+ const fn = pipe(start, addOne);
35
+
36
+ expect(fn()).toBe(4);
37
+ });
38
+
31
39
  });
@@ -1,4 +1,5 @@
1
1
  type UnaryFn<A, R> = (a: A) => R;
2
+ type ZeroFn<R> = () => R;
2
3
  type PipeInput<Fns extends UnaryFn<any, any>[]> = Fns extends [UnaryFn<infer A, any>, ...UnaryFn<any, any>[]]
3
4
  ? A
4
5
  : never;
@@ -11,6 +12,17 @@ type PipeOutput<Fns extends UnaryFn<any, any>[]> = Fns extends [UnaryFn<any, inf
11
12
  : never;
12
13
  type Pipe<Fns extends UnaryFn<any, any>[]> = (input: PipeInput<Fns>) => PipeOutput<Fns>;
13
14
 
15
+ function pipe<R>(ab: ZeroFn<R>): () => R;
16
+ function pipe<B, R>(ab: ZeroFn<B>, bc: UnaryFn<B, R>): () => R;
17
+ function pipe<B, C, R>(ab: ZeroFn<B>, bc: UnaryFn<B, C>, cd: UnaryFn<C, R>): () => R;
18
+ function pipe<B, C, D, R>(ab: ZeroFn<B>, bc: UnaryFn<B, C>, cd: UnaryFn<C, D>, de: UnaryFn<D, R>): () => R;
19
+ function pipe<B, C, D, E, R>(
20
+ ab: ZeroFn<B>,
21
+ bc: UnaryFn<B, C>,
22
+ cd: UnaryFn<C, D>,
23
+ de: UnaryFn<D, E>,
24
+ ef: UnaryFn<E, R>
25
+ ): () => R;
14
26
  function pipe<A, R>(ab: UnaryFn<A, R>): (a: A) => R;
15
27
  function pipe<A, B, R>(ab: UnaryFn<A, B>, bc: UnaryFn<B, R>): (a: A) => R;
16
28
  function pipe<A, B, C, R>(ab: UnaryFn<A, B>, bc: UnaryFn<B, C>, cd: UnaryFn<C, R>): (a: A) => R;
@@ -1,13 +1,17 @@
1
1
  import SideEffect from './sideEffect';
2
2
  import pipe from './pipe';
3
3
  import pipeSideEffect from './pipeSideEffect';
4
+ import pipeSideEffectStrict from './pipeSideEffectStrict';
4
5
  import pipeAsync from '../async/pipeAsync';
5
6
  import pipeAsyncSideEffect from '../async/pipeAsyncSideEffect';
7
+ import pipeAsyncSideEffectStrict from '../async/pipeAsyncSideEffectStrict';
6
8
 
7
9
  type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
8
10
  ? true
9
11
  : false;
10
12
  type Expect<T extends true> = T;
13
+ type EffectUnion<T> = Extract<T, SideEffect<any>> extends SideEffect<infer E> ? E : never;
14
+ type ValueUnion<T> = Exclude<T, SideEffect<any>>;
11
15
 
12
16
  export const sideEffectInput = SideEffect.of(() => 0);
13
17
 
@@ -52,3 +56,48 @@ type PipeAsyncExpected = (input: number | SideEffect<any>) => Promise<string | S
52
56
  export type PipeAsyncAcceptsSideEffectInput = Expect<
53
57
  Equal<typeof pipeAsyncWithSideEffectInput, PipeAsyncExpected>
54
58
  >;
59
+
60
+ export const strictPipeSideEffect = pipeSideEffectStrict(
61
+ (value: number) => value + 1,
62
+ (value) => (value > 1 ? value : SideEffect.of(() => 'LOW' as const)),
63
+ (value) => (value > 2 ? value : SideEffect.of(() => 0 as const))
64
+ );
65
+
66
+ export const strictPipeSideEffectResult = strictPipeSideEffect(1);
67
+
68
+ type StrictSideEffectEffects = EffectUnion<typeof strictPipeSideEffectResult>;
69
+ type StrictSideEffectEffectsExpected = 'LOW' | 0;
70
+ export type PipeSideEffectStrictEffects = Expect<Equal<StrictSideEffectEffects, StrictSideEffectEffectsExpected>>;
71
+
72
+ type StrictSideEffectValue = ValueUnion<typeof strictPipeSideEffectResult>;
73
+ type StrictSideEffectValueExpected = number;
74
+ export type PipeSideEffectStrictValue = Expect<Equal<StrictSideEffectValue, StrictSideEffectValueExpected>>;
75
+
76
+ export const strictPipeSideEffectInput = strictPipeSideEffect(SideEffect.of(() => 'INPUT' as const));
77
+
78
+ type StrictSideEffectInputEffects = EffectUnion<typeof strictPipeSideEffectInput>;
79
+ type StrictSideEffectInputEffectsExpected = 'LOW' | 0 | 'INPUT';
80
+ export type PipeSideEffectStrictInputEffects = Expect<
81
+ Equal<StrictSideEffectInputEffects, StrictSideEffectInputEffectsExpected>
82
+ >;
83
+
84
+ export const strictPipeAsyncSideEffect = pipeAsyncSideEffectStrict(
85
+ (value: number) => value + 1,
86
+ async (value) => (value > 1 ? value : SideEffect.of(() => 'LOW' as const)),
87
+ (value) => (value > 2 ? value : SideEffect.of(() => 0 as const))
88
+ );
89
+
90
+ export const strictPipeAsyncSideEffectResult = strictPipeAsyncSideEffect(1);
91
+
92
+ type StrictPipeAsyncResolved = Awaited<typeof strictPipeAsyncSideEffectResult>;
93
+ type StrictPipeAsyncEffects = EffectUnion<StrictPipeAsyncResolved>;
94
+ type StrictPipeAsyncEffectsExpected = 'LOW' | 0;
95
+ export type PipeAsyncSideEffectStrictEffects = Expect<
96
+ Equal<StrictPipeAsyncEffects, StrictPipeAsyncEffectsExpected>
97
+ >;
98
+
99
+ type StrictPipeAsyncValue = ValueUnion<StrictPipeAsyncResolved>;
100
+ type StrictPipeAsyncValueExpected = number;
101
+ export type PipeAsyncSideEffectStrictValue = Expect<
102
+ Equal<StrictPipeAsyncValue, StrictPipeAsyncValueExpected>
103
+ >;
@@ -20,4 +20,12 @@ describe('pipeSideEffect', () => {
20
20
 
21
21
  expect(fn(effect)).toBe(effect);
22
22
  });
23
+
24
+ it('supports zero-arity starts', () => {
25
+ const start = () => 3;
26
+ const addOne = (n: number) => n + 1;
27
+ const fn = pipeSideEffect(start, addOne);
28
+
29
+ expect(fn()).toBe(4);
30
+ });
23
31
  });
@@ -3,6 +3,7 @@ import SideEffect, { isSideEffect } from './sideEffect';
3
3
  type MaybeSideEffect<T> = T | SideEffect<any>;
4
4
  type NonSideEffect<T> = Exclude<T, SideEffect<any>>;
5
5
  type UnaryFn<A, R> = (a: A) => MaybeSideEffect<R>;
6
+ type ZeroFn<R> = () => MaybeSideEffect<R>;
6
7
  type PipeInput<Fns extends UnaryFn<any, any>[]> = Fns extends [UnaryFn<infer A, any>, ...UnaryFn<any, any>[]]
7
8
  ? A
8
9
  : never;
@@ -18,6 +19,22 @@ type PipeSideEffect<Fns extends UnaryFn<any, any>[]> = (
18
19
  input: PipeInput<Fns> | SideEffect<any>
19
20
  ) => Resolve<PipeOutput<Fns>>;
20
21
 
22
+ function pipeSideEffect<R>(ab: ZeroFn<R>): () => MaybeSideEffect<R>;
23
+ function pipeSideEffect<B, R>(ab: ZeroFn<B>, bc: UnaryFn<B, R>): () => MaybeSideEffect<R>;
24
+ function pipeSideEffect<B, C, R>(ab: ZeroFn<B>, bc: UnaryFn<B, C>, cd: UnaryFn<C, R>): () => MaybeSideEffect<R>;
25
+ function pipeSideEffect<B, C, D, R>(
26
+ ab: ZeroFn<B>,
27
+ bc: UnaryFn<B, C>,
28
+ cd: UnaryFn<C, D>,
29
+ de: UnaryFn<D, R>
30
+ ): () => MaybeSideEffect<R>;
31
+ function pipeSideEffect<B, C, D, E, R>(
32
+ ab: ZeroFn<B>,
33
+ bc: UnaryFn<B, C>,
34
+ cd: UnaryFn<C, D>,
35
+ de: UnaryFn<D, E>,
36
+ ef: UnaryFn<E, R>
37
+ ): () => MaybeSideEffect<R>;
21
38
  function pipeSideEffect<A, R>(ab: UnaryFn<A, R>): (a: A | SideEffect<any>) => MaybeSideEffect<R>;
22
39
  function pipeSideEffect<A, B, R>(
23
40
  ab: UnaryFn<A, B>,
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import pipeSideEffectStrict from './pipeSideEffectStrict';
3
+ import SideEffect from './sideEffect';
4
+
5
+ describe('pipeSideEffectStrict', () => {
6
+ it('short-circuits when SideEffect is returned', () => {
7
+ const effect = new SideEffect(() => 'effect');
8
+ const stop = (_value: number) => effect;
9
+ const after = (_value: number) => _value + 1;
10
+
11
+ const fn = pipeSideEffectStrict((n: number) => n + 1, stop, after);
12
+ const result = fn(1);
13
+
14
+ expect(result).toBe(effect);
15
+ });
16
+
17
+ it('returns SideEffect inputs without executing the pipeline', () => {
18
+ const effect = new SideEffect(() => 'effect');
19
+ const fn = pipeSideEffectStrict((n: number) => n + 1);
20
+
21
+ expect(fn(effect)).toBe(effect);
22
+ });
23
+ });