fp-pack 0.9.1 → 0.9.2

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.
@@ -1,1752 +1,432 @@
1
1
  # fp-pack AI Agent Skills
2
2
 
3
- Document Version: 0.9.1
3
+ Document Version: 0.9.2
4
4
 
5
- ## ⚠️ Activation Condition
5
+ ## ⚠️ Activation Condition (Read First)
6
6
 
7
- **These guidelines apply ONLY when fp-pack is installed in the current project.**
7
+ These guidelines apply **only when `fp-pack` is installed** in the current project.
8
8
 
9
- Before following these instructions, verify fp-pack availability:
10
- - Check `package.json` for fp-pack in dependencies or devDependencies
11
- - Check if `node_modules/fp-pack` exists
12
- - Check if existing code imports from fp-pack
9
+ Before following this document:
10
+ - Check `package.json` for `fp-pack` in dependencies/devDependencies
11
+ - Check `node_modules/fp-pack` exists
12
+ - Check existing code imports from `fp-pack` or `fp-pack/stream`
13
13
 
14
- If fp-pack is NOT installed, use standard coding practices for the project. Do NOT suggest installing fp-pack unless explicitly requested by the user.
14
+ If `fp-pack` is **not** installed, use the project’s existing conventions. Do **not** suggest adding fp-pack unless the user asks.
15
15
 
16
16
  ---
17
17
 
18
- 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.
18
+ ## Agent Checklist (TL;DR)
19
19
 
20
- ## Project Philosophy
20
+ When writing or editing code in an fp-pack project:
21
+ - Prefer `pipe` / `pipeAsync` for composition; keep helpers **unary** in the pipeline.
22
+ - Prefer **data-last** utilities (curried) so they compose: `map(fn)`, `prop('k')`, `assoc('k', v)`, etc.
23
+ - Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
24
+ - Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
25
+ - **Never call `runPipeResult` inside a pipeline**; call it at the boundary (event handler, service wrapper, etc.).
26
+ - If TypeScript inference gets stuck with a data-last generic, use a small wrapper, an explicit type hint, or `pipeHint`.
21
27
 
22
- fp-pack is a TypeScript functional programming library focused on:
28
+ ## Philosophy (What fp-pack Optimizes For)
23
29
 
24
- 1. **Function Composition**: Use `pipe` and `pipeAsync` as the primary tools for combining operations
25
- 2. **Declarative Code**: Prefer function composition over imperative loops and mutations
26
- 3. **No Monad Pattern**: Traditional FP monads (Option, Either, etc.) are NOT used - they don't compose well with `pipe`
27
- 4. **SideEffect Pattern**: Handle errors and side effects using `SideEffect` with `pipeSideEffect` / `pipeAsyncSideEffect` pipelines (use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict` for strict unions)
28
- 5. **Lazy Evaluation**: Use `stream/*` functions for efficient iterable processing
30
+ fp-pack is a TypeScript FP utility library focused on:
31
+ 1) Function composition via `pipe` / `pipeAsync`
32
+ 2) Declarative, immutable transforms (avoid loops and mutations)
33
+ 3) Practical TypeScript inference (data-last + currying for most helpers)
34
+ 4) Optional early-exit handling via `SideEffect` (only when needed)
35
+ 5) Lazy iterable processing via `fp-pack/stream` for performance-sensitive flows
29
36
 
30
- ## Core Composition Functions
37
+ ---
38
+
39
+ ## Core Composition
31
40
 
32
- ### pipe - Synchronous Function Composition
41
+ ### `pipe` (sync)
33
42
 
34
- **Always prefer `pipe` for synchronous operations** instead of manual imperative code.
43
+ Use `pipe` to build a unary function:
35
44
 
36
- ```typescript
37
- import { pipe, map, filter, take } from 'fp-pack';
45
+ ```ts
46
+ import { pipe, filter, map, take } from 'fp-pack';
38
47
 
39
- // GOOD: Declarative pipe composition
40
48
  const processUsers = pipe(
41
- filter((user: User) => user.age >= 18),
42
- map(user => user.name.toUpperCase()),
49
+ filter((u: User) => u.active),
50
+ map((u) => u.name),
43
51
  take(10)
44
52
  );
45
-
46
- // BAD: Imperative approach
47
- const processUsers = (users: User[]) => {
48
- const result = [];
49
- for (const user of users) {
50
- if (user.age >= 18) {
51
- result.push(user.name.toUpperCase());
52
- if (result.length >= 10) break;
53
- }
54
- }
55
- return result;
56
- };
57
53
  ```
58
54
 
59
- > For SideEffect-based early exits, use `pipeSideEffect` (or `pipeSideEffectStrict` when you want strict unions).
55
+ ### `pipeAsync` (async)
60
56
 
61
- ### pipeAsync - Asynchronous Function Composition
57
+ Use `pipeAsync` when any step is async:
62
58
 
63
- **Use `pipeAsync` for any async operations** including API calls, database queries, or async transformations.
64
-
65
- ```typescript
59
+ ```ts
66
60
  import { pipeAsync } from 'fp-pack';
67
61
 
68
- // GOOD: Async pipe composition
69
- const fetchUserData = pipeAsync(
70
- async (userId: string) => fetch(`/api/users/${userId}`),
71
- async (response) => response.json(),
62
+ const fetchUser = pipeAsync(
63
+ async (id: string) => fetch(`/api/users/${id}`),
64
+ async (res) => res.json(),
72
65
  (data) => data.user
73
66
  );
74
-
75
- // BAD: Manual async handling
76
- const fetchUserData = async (userId: string) => {
77
- const response = await fetch(`/api/users/${userId}`);
78
- const data = await response.json();
79
- return data.user;
80
- };
81
67
  ```
82
68
 
83
- > For SideEffect-aware async pipelines, use `pipeAsyncSideEffect` (or `pipeAsyncSideEffectStrict` for strict unions).
69
+ ---
70
+
71
+ ## Currying & Data-Last Conventions
84
72
 
85
- ## Data-last Generic Inference Caveats
73
+ Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
74
+
75
+ - Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
76
+ - Avoid in pipelines: helpers that require you to pass the data first, or that return a function whose type depends on the final data arg (see next section)
77
+
78
+ Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
79
+
80
+ ---
86
81
 
87
- Some data-last helpers return a **generic function** whose type is determined only by the final data argument. Inside
88
- `pipe`/`pipeAsync`, TypeScript cannot infer that type, so you may need `pipeHint`, a small type hint, or a data-first wrapper.
82
+ ## TypeScript: Data-last Generic Inference Caveats (Important)
89
83
 
90
- ```typescript
84
+ Some data-last helpers return a **generic function** whose type is only determined by the final data argument.
85
+ Inside `pipe`/`pipeAsync`, TypeScript sometimes can’t infer that type, so you may need a tiny hint.
86
+
87
+ ### Recommended fixes (choose 1)
88
+
89
+ ```ts
91
90
  import { pipe, pipeHint, zip, some } from 'fp-pack';
92
91
 
93
- // Option 1: data-first wrapper
92
+ // 1) data-first wrapper (most explicit)
94
93
  const withWrapper = pipe(
95
94
  (values: number[]) => zip([1, 2, 3], values),
96
95
  some(([a, b]) => a > b)
97
96
  );
98
97
 
99
- // Option 2: explicit type annotation
98
+ // 2) explicit function type hint (short, but uses an assertion)
100
99
  const withHint = pipe(
101
100
  zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
102
101
  some(([a, b]) => a > b)
103
102
  );
104
103
 
105
- // Option 3: pipeHint helper
104
+ // 3) pipeHint helper (keeps pipeline style)
106
105
  const withPipeHint = pipe(
107
106
  pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
108
107
  some(([a, b]) => a > b)
109
108
  );
110
109
  ```
111
110
 
112
- **Utilities that may need a type hint in data-last pipelines:**
113
- - **Array**: `chunk`, `drop`, `take`, `zip`
114
- - **Object**: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
115
- - **Async**: `timeout`
116
- - **Stream**: `chunk`, `drop`, `take`, `zip`
111
+ ### Utilities that may need a hint in data-last pipelines
117
112
 
118
- ## SideEffect Pattern - For Special Cases Only
113
+ - Array: `chunk`, `drop`, `take`, `zip`
114
+ - Object: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
115
+ - Async: `timeout`
116
+ - Stream: `chunk`, `drop`, `take`, `zip`
119
117
 
120
- **Most cases: Use `pipe` / `pipeAsync` - they're simpler and sufficient for 99% of use cases.**
118
+ When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
121
119
 
122
- `pipe` and `pipeAsync` are for **pure** functions and don't handle `SideEffect`. **Only use `pipeSideEffect`/`pipeAsyncSideEffect` when you specifically need**:
123
- - Early termination based on validation
124
- - Error handling with side effects (logging, toasts, etc.)
125
- - Optional chaining patterns
126
-
127
- For regular error handling, standard try-catch or error propagation is perfectly fine.
128
- If you want precise SideEffect unions across branches, use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`.
120
+ ---
129
121
 
130
- ```typescript
131
- // MOST CASES: Just use pipe with regular error handling
132
- import { pipe, map, filter } from 'fp-pack';
122
+ ## SideEffect Pattern (Use Only When Needed)
133
123
 
134
- const processData = pipe(
135
- validateInput,
136
- transformData,
137
- saveData
138
- );
124
+ Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you truly need **early termination**:
125
+ - validation pipelines that should stop early
126
+ - recoverable errors you want to model as data
127
+ - branching flows where you want to short-circuit
139
128
 
140
- try {
141
- const result = processData(input);
142
- } catch (error) {
143
- console.error('Processing failed:', error);
144
- }
129
+ ### SideEffect-aware pipes
130
+ - `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
131
+ - `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended when you care about types)
145
132
 
146
- // SPECIAL CASES: Use pipeSideEffect when you need early termination with side effects
147
- import { pipeSideEffect, SideEffect, runPipeResult } from 'fp-pack';
148
-
149
- const processDataPipeline = pipeSideEffect(
150
- validateInput,
151
- (data) => {
152
- if (!data.isValid) {
153
- return SideEffect.of(() => {
154
- showToast('Invalid data'); // Side effect
155
- logError('validation_failed'); // Side effect
156
- return null;
157
- });
158
- }
159
- return data;
160
- },
161
- transformData
162
- );
133
+ ### Rules
134
+ - Do **not** call `runPipeResult` or `matchSideEffect` **inside** the pipeline.
135
+ - Prefer `isSideEffect` for precise runtime narrowing in both branches.
136
+ - Use `runPipeResult` only at the boundary, and provide generics if inference is widened.
163
137
 
164
- // runPipeResult must be called OUTSIDE the pipeline
165
- const finalValue = runPipeResult(processDataPipeline(input));
166
- ```
138
+ ### Key functions
139
+ - `SideEffect.of(effectFn, label?)`
140
+ - `isSideEffect(value)` (type guard)
141
+ - `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
142
+ - `matchSideEffect(result, { value, effect })`
167
143
 
168
- **Key SideEffect functions:**
169
- - `SideEffect.of(fn, label?)` - Create a side effect container
170
- - `isSideEffect(value)` - Type guard for **runtime checking** whether a value is a SideEffect
171
- - `runPipeResult<T, R>(result)` - Execute SideEffect or return value (call **OUTSIDE** pipelines). If the input is narrowed to `SideEffect<R>` (e.g. after `isSideEffect`), it returns `R`. If the input is widened to `SideEffect<any>` or `any`, the result becomes `any` unless you provide generics.
172
- - `matchSideEffect(result, { value, effect })` - Pattern match on result
144
+ ### `runPipeResult` type behavior (important)
173
145
 
174
- **Type-safe result handling:**
146
+ - If the input is **narrowed** to `SideEffect<R>` (e.g. inside `if (isSideEffect(x))`), `runPipeResult(x)` returns `R`.
147
+ - If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
175
148
 
176
- ```typescript
177
- import { pipeSideEffect, pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
149
+ ```ts
150
+ import {
151
+ pipeSideEffect,
152
+ pipeSideEffectStrict,
153
+ SideEffect,
154
+ isSideEffect,
155
+ runPipeResult,
156
+ } from 'fp-pack';
178
157
 
179
- const processNumbers = pipeSideEffectStrict(
180
- (nums: number[]) => nums.filter(n => n % 2 === 1),
181
- (odds) => odds.length > 0
182
- ? odds
183
- : SideEffect.of(() => 'No odd numbers'),
184
- (odds) => odds.map(n => n * 2)
158
+ const nonStrict = pipeSideEffect(
159
+ (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
185
160
  );
161
+ const widened: number | SideEffect<any> = nonStrict(-1);
162
+ const unsafe = runPipeResult(widened); // any
186
163
 
187
- const result = processNumbers([1, 2, 3, 4, 5]);
164
+ const strict = pipeSideEffectStrict(
165
+ (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
166
+ );
167
+ const precise = strict(-1); // number | SideEffect<'NEG'>
188
168
 
189
- // ✅ CORRECT: Use isSideEffect for runtime checking
190
- if (!isSideEffect(result)) {
191
- // TypeScript knows: result is number[]
192
- const sum: number = result.reduce((a, b) => a + b, 0);
193
- } else {
194
- // TypeScript knows: result is SideEffect<'No odd numbers'>
195
- const error = runPipeResult(result); // 'No odd numbers'
169
+ if (isSideEffect(precise)) {
170
+ const err = runPipeResult(precise); // 'NEG'
196
171
  }
197
-
198
- // ⚠️ Non-strict pipeSideEffect widens SideEffect to any
199
- const widened: number[] | SideEffect<any> = pipeSideEffect(
200
- (nums: number[]) => nums,
201
- (nums) => nums.length > 0 ? nums : SideEffect.of(() => 'EMPTY')
202
- )([]);
203
- const unsafeValue = runPipeResult(widened); // any
204
-
205
- // ✅ CORRECT: Provide generics to recover a safe union
206
- const safeValue = runPipeResult<number[], string>(result); // result: number[] | string (union type - safe but not narrowed)
207
172
  ```
208
173
 
209
- **⚠️ CRITICAL: runPipeResult Type Safety**
210
-
211
- `runPipeResult<T, R=any>` has a default type parameter `R=any`. This means:
212
-
213
- - ✅ **Precise input types**: `T | SideEffect<'E'>` preserves `T | 'E'` without extra annotations.
214
- - ⚠️ **Widened inputs**: `T | SideEffect<any>` (or `any`) collapses to `any`.
215
- - ✅ **With generics**: `runPipeResult<SuccessType, ErrorType>(result)` restores a safe union when inference is lost.
216
- - ✅ **After narrowing**: If the input is `SideEffect<'E'>`, `runPipeResult` returns `'E'`.
217
- - ✅ **With isSideEffect**: Prefer for runtime narrowing when you need branch-specific types.
174
+ ---
218
175
 
219
- Provide generics when inference is lost; prefer `isSideEffect` for precise narrowing.
176
+ ## Stream Functions (`fp-pack/stream`) Lazy Iterables
220
177
 
221
- ## Stream Functions - Lazy Iterable Processing
178
+ Use stream utilities when:
179
+ - data is large or unbounded (`range`, streams, generators)
180
+ - you want lazy evaluation (avoid allocating intermediate arrays)
181
+ - you want to support `Iterable` and `AsyncIterable`
222
182
 
223
- **Use `stream/*` functions for lazy, memory-efficient data processing** instead of array methods.
183
+ Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
224
184
 
225
- ```typescript
185
+ ```ts
226
186
  import { pipe } from 'fp-pack';
227
- import { map, filter, take, toArray, range } from 'fp-pack/stream';
187
+ import { range, filter, map, take, toArray } from 'fp-pack/stream';
228
188
 
229
- // GOOD: Lazy stream processing
230
- const processLargeDataset = pipe(
189
+ const first100SquaresOfEvens = pipe(
231
190
  filter((n: number) => n % 2 === 0),
232
- map(n => n * n),
191
+ map((n) => n * n),
233
192
  take(100),
234
193
  toArray
235
194
  );
236
195
 
237
- // Processes only what's needed - memory efficient
238
- const result = processLargeDataset(range(1, 1000000));
239
-
240
- // BAD: Eager array processing
241
- const result = Array.from({ length: 1000000 }, (_, i) => i + 1)
242
- .filter(n => n % 2 === 0)
243
- .map(n => n * n)
244
- .slice(0, 100); // Processed entire dataset!
196
+ const result = first100SquaresOfEvens(range(Infinity));
245
197
  ```
246
198
 
247
- **Stream functions support both sync and async iterables:**
248
- - Sync: `Iterable<T>` → `IterableIterator<R>`
249
- - Async: `AsyncIterable<T>` → `AsyncIterableIterator<R>`
199
+ ---
250
200
 
251
- ## Available Functions by Category
201
+ ## Available Functions (Practical Index)
252
202
 
253
- ### Composition
254
- - `pipe` - Left-to-right function composition (sync)
255
- - `pipeSideEffect` - Left-to-right composition with SideEffect short-circuiting
256
- - `pipeSideEffectStrict` - SideEffect composition with strict effect unions
257
- - `compose` - Right-to-left function composition
258
- - `curry` - Curry a function
259
- - `partial` - Partial application
260
- - `flip` - Flip function argument order
261
- - `complement` - Logical negation
262
- - `identity` - Return input unchanged
263
- - `constant` - Always return the same value
264
- - `from` - Ignore input and return a fixed value
265
- - `tap` - Execute side effect and return original value
266
- - `tap0` - Execute side effect without input
267
- - `once` - Execute function only once
268
- - `memoize` - Cache function results
269
- - `SideEffect` - Side effect container
270
- - `isSideEffect` - Type guard for SideEffect
271
- - `matchSideEffect` - Pattern match on value/SideEffect
272
- - `runPipeResult` - Execute SideEffect or return value
203
+ This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
273
204
 
274
- ### Async
275
- - `pipeAsync` - Async function composition
276
- - `pipeAsyncSideEffect` - Async composition with SideEffect short-circuiting
277
- - `pipeAsyncSideEffectStrict` - Async SideEffect composition with strict effect unions
278
- - `delay` - Delay execution
279
- - `timeout` - Add timeout to promise
280
- - `retry` - Retry failed operations
281
- - `debounce` - Debounce function calls
282
- - `debounceLeading` - Debounce with leading edge
283
- - `debounceLeadingTrailing` - Debounce with both edges
284
- - `throttle` - Throttle function calls
205
+ ### Composition
206
+ - `pipe`, `pipeAsync`
207
+ - SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
208
+ - Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `flip`, `partial`, `curry`, `compose`, `complement`
209
+ - SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
285
210
 
286
211
  ### Array
287
- - `map`, `filter`, `reduce`, `flatMap`
288
- - `find`, `some`, `every`
289
- - `take`, `drop`, `takeWhile`, `dropWhile`
290
- - `chunk`, `zip`, `zipWith`, `unzip`, `zipIndex`
291
- - `uniq`, `uniqBy`, `sort`, `sortBy`, `groupBy`
292
- - `concat`, `append`, `prepend`, `flatten`, `flattenDeep`
293
- - `head`, `tail`, `last`, `init`
294
- - `range`, `partition`, `scan`
212
+ - Core transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
213
+ - Queries: `find`, `some`, `every`
214
+ - Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
215
+ - Ordering/grouping: `sort`, `sortBy`, `groupBy`, `uniqBy`
216
+ - Pairing: `zip`, `zipWith`
217
+ - Combining: `concat`, `append`, `prepend`, `flatten`
295
218
 
296
219
  ### Object
297
- - `prop`, `propOr`, `path`, `pathOr`
298
- - `pick`, `omit`
299
- - `assoc`, `assocPath`, `dissoc`, `dissocPath`
300
- - `merge`, `mergeDeep`, `mergeAll`
301
- - `keys`, `values`, `entries`
302
- - `mapValues`, `evolve`
303
- - `has`, `hasPath`
220
+ - Access: `prop`, `propOr`, `propStrict`, `path`, `pathOr`
221
+ - Pick/drop: `pick`, `omit`
222
+ - Updates: `assoc`, `assocPath`, `dissocPath`
223
+ - Merge: `merge`, `mergeDeep`
224
+ - Transforms: `mapValues`, `evolve`
225
+ - Predicates: `has`
304
226
 
305
227
  ### Control Flow
306
- - `ifElse` - Conditional branching
307
- - `when`, `unless` - Conditional execution
308
- - `cond` - Multi-branch conditional
309
- - `tryCatch` - Safe function execution
310
- - `guard` - Validation guard
311
-
312
- ### Stream (Lazy Iterables)
313
- - `append`, `concat`, `prepend`
314
- - `map`, `filter`, `flatMap`, `flatten`, `flattenDeep`
315
- - `take`, `takeWhile`, `drop`, `dropWhile`, `chunk`
316
- - `zip`, `zipWith`, `find`, `some`, `every`
317
- - `reduce`, `scan`
318
- - `range`
319
- - `toArray` - Materialize stream to array
320
- - `toAsync` - Convert to async iterable
321
-
322
- ### Math
323
- - `add`, `sub`, `mul`, `div`
324
- - `sum`, `mean`, `min`, `max`
325
- - `round`, `floor`, `ceil`, `randomInt`
326
-
327
- ### String
328
- - `trim`, `split`, `join`, `replace`
329
- - `toUpper`, `toLower`
330
- - `startsWith`, `endsWith`, `match`
331
-
332
- ### Equality
333
- - `equals`, `includes`
334
- - `isNil`, `isEmpty`, `isType`
335
- - `gt`, `gte`, `lt`, `lte`
336
- - `clamp`
337
-
338
- ### Nullable
339
- - `maybe`, `mapMaybe`, `getOrElse`, `fold`, `result`
340
-
341
- ### Debug
342
- - `assert`, `invariant`, `log`
343
-
344
- ## Coding Guidelines for AI Agents
345
-
346
- ### 0. Detect Project Language (JS vs TS)
347
-
348
- Before writing code, check whether the project is JavaScript or TypeScript:
349
-
350
- - **TypeScript projects**: use explicit types, leverage generics, and keep type-safe signatures in examples.
351
- - **JavaScript projects**: avoid TypeScript-only syntax and prefer JSDoc only when it adds clarity or is already used.
352
-
353
- ### 0.1 Quick Signature Lookup
228
+ - `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
354
229
 
355
- If a function signature or argument order is unclear, check the local declaration or source files:
356
-
357
- - Main exports: `dist/index.d.ts`
358
- - Stream exports: `dist/stream/index.d.ts`
359
- - Main utilities (fallback): `src/implement/**`
360
- - Stream utilities (fallback): `src/stream/**`
361
- - Installed package:
362
- - `node_modules/fp-pack/dist/index.d.ts`
363
- - `node_modules/fp-pack/dist/stream/index.d.ts`
364
- - `node_modules/fp-pack/src/implement/**`
365
- - `node_modules/fp-pack/src/stream/**`
366
-
367
- ### 1. Always Prefer pipe/pipeAsync
368
-
369
- ```typescript
370
- // GOOD
371
- const result = pipe(
372
- trim,
373
- split(','),
374
- map(toNumber),
375
- filter(isPositive)
376
- )(input);
377
-
378
- // BAD
379
- const trimmed = trim(input);
380
- const parts = split(',')(trimmed);
381
- const numbers = map(toNumber)(parts);
382
- const result = filter(isPositive)(numbers);
383
- ```
384
-
385
- ### 2. Use Curried Functions (Where Available)
386
-
387
- Most multi-arg functions are curried. Single-arg utilities are already unary, so currying adds no benefit; use them directly (e.g. `uniq`, `flatten`, `flattenDeep`, `head`, `tail`, `last`, `init`, `range`, `sum`, `mean`, `min`, `max`, `round`, `floor`, `ceil`, `trim`, `toLower`, `toUpper`, `isNil`, `isEmpty`, `isType`). These are already pipe-friendly without a curried variant.
230
+ ### Async
231
+ - `retry`, `timeout`, `delay`
232
+ - Function-returning helpers: `debounce*`, `throttle`
388
233
 
389
- ```typescript
390
- import { pipe, map, filter } from 'fp-pack';
234
+ ### Stream (Lazy Iterables)
235
+ - Building: `range`
236
+ - Transforms: `map`, `filter`, `flatMap`, `flatten`
237
+ - Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
238
+ - Queries/reductions: `find`, `some`, `every`, `reduce`, `scan`
239
+ - Pairing/combining: `zip`, `zipWith`, `concat`, `append`, `prepend`
240
+ - Utilities: `toArray`, `toAsync`
241
+
242
+ ### Math / String / Equality / Debug
243
+ - Math: `add`, `sub`, `mul`, `div`, `randomInt`, `clamp`
244
+ - String: `split`, `join`, `replace`, `match`, `trim`
245
+ - Equality: `equals`, `isNil`
246
+ - Debug: `assert`, `invariant`
391
247
 
392
- // GOOD: Curried usage in pipe
393
- const processUsers = pipe(
394
- filter(user => user.active),
395
- map(user => user.name)
396
- );
248
+ ---
397
249
 
398
- // GOOD: Partial application
399
- const filterActive = filter((user: User) => user.active);
400
- const getNames = map((user: User) => user.name);
401
- const processUsers = pipe(filterActive, getNames);
402
- ```
250
+ ## Micro-Patterns (Keep It Short)
403
251
 
404
- ### 2.1 Custom Utility Authoring (Curry Typing)
252
+ ### Boundary handling (UI/event/service)
405
253
 
406
- When you add your own helpers for `pipe`, follow these rules:
254
+ Do the work in a pipeline, unwrap at the boundary:
407
255
 
408
- - Keep **data-last** argument order.
409
- - **Curry multi-arg functions** so they compose well.
410
- - **Fixed signatures** can use `curry(fn)` directly.
411
- - **Generic or overloaded signatures** should use an explicit type alias + cast to preserve inference.
256
+ ```ts
257
+ import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
412
258
 
413
- ```typescript
414
- // Fixed signature: curry is enough
415
- function split(separator: string, str: string): string[] {
416
- return str.split(separator);
417
- }
418
- export default curry(split);
259
+ const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
260
+ const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
419
261
 
420
- // Generic signature: add a type alias for the curried form
421
- type Chunk = {
422
- (size: number): <T>(arr: T[]) => T[][];
423
- <T>(size: number, arr: T[]): T[][];
262
+ export const handler = (n: number) => {
263
+ const result = pipeline(n);
264
+ if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
265
+ return result; // number
424
266
  };
425
-
426
- function chunk<T>(size: number, arr: T[]): T[][] {
427
- // ...
428
- return [];
429
- }
430
-
431
- const curriedChunk = curry(chunk) as Chunk;
432
- export default curriedChunk;
433
267
  ```
434
268
 
435
- ### 3. Choose pipe vs pipeSideEffect
436
-
437
- **Default choice: Start with `pipe` / `pipeAsync`**
438
-
439
- Most data transformations are pure and don't need SideEffect handling. Use `pipe` for sync operations and `pipeAsync` for async operations. **Only switch to SideEffect-aware pipes when you actually need** early termination or error handling with side effects.
269
+ ### Data-first style with `from()`
440
270
 
441
- - **`pipe`** - Synchronous, **pure** transformations (99% of cases)
442
- - **`pipeAsync`** - Async, **pure** transformations (99% of cases)
443
- - **`pipeSideEffect`** - **Only when you need** SideEffect short-circuiting (sync)
444
- - **`pipeAsyncSideEffect`** - **Only when you need** SideEffect short-circuiting (async)
445
- - **`pipeSideEffectStrict`** - Sync SideEffect pipelines with strict effect unions
446
- - **`pipeAsyncSideEffectStrict`** - Async SideEffect pipelines with strict effect unions
271
+ Some agents/users prefer “data-first” pipelines. You can keep `pipe` data-last and still write data-first flows by injecting the initial value with `from()`:
447
272
 
448
- **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.
273
+ ```ts
274
+ import { pipe, from, map, filter, take } from 'fp-pack';
449
275
 
450
- ```typescript
451
- // Sync: use pipe
452
- const processNumbers = pipe(
453
- map((n: number) => n * 2),
454
- filter(n => n > 10)
455
- );
276
+ // data-first feeling: start with data, then compose transforms
277
+ const result = pipe(
278
+ from([1, 2, 3, 4, 5]),
279
+ filter((n: number) => n % 2 === 0),
280
+ map((n) => n * 10),
281
+ take(2)
282
+ )(); // [20, 40]
456
283
 
457
- // Async: use pipeAsync
458
- const processUsers = pipeAsync(
459
- async (ids: string[]) => db.users.findMany(ids),
460
- map(user => user.profile),
461
- filter(profile => profile.verified)
284
+ // same idea when you already have an input (normal usage)
285
+ const process = pipe(
286
+ filter((n: number) => n % 2 === 0),
287
+ map((n) => n * 10)
462
288
  );
289
+ const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
463
290
  ```
464
291
 
465
- ### 3.1. SideEffect Composition Rule
466
-
467
- **🔄 Critical Rule: SideEffect Contagion**
468
-
469
- 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.
470
-
471
- 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`.
292
+ ### `ifElse` uses functions (not values)
472
293
 
473
- ```typescript
474
- import { pipe, pipeSideEffect, SideEffect } from 'fp-pack';
475
-
476
- const validateUserPipeline = pipeSideEffect(
477
- findUser,
478
- validateAge
479
- );
480
- // Result type: User | SideEffect
481
-
482
- // ❌ WRONG - pipe cannot handle SideEffect
483
- const wrongPipeline = pipe(
484
- validateUserPipeline, // Returns User | SideEffect
485
- (user) => user.email // Type error! SideEffect has no 'email' property
486
- );
294
+ ```ts
295
+ import { ifElse, from } from 'fp-pack';
487
296
 
488
- // CORRECT - Keep using pipeSideEffect
489
- const correctPipeline = pipeSideEffect(
490
- validateUserPipeline, // User | SideEffect - handled correctly
491
- (user) => user.email, // Automatically skipped if SideEffect
492
- sendEmail
493
- );
494
-
495
- // The same rule applies to async pipes
496
- const asyncPipeline = pipeAsyncSideEffect(
497
- fetchUser,
498
- validateUser
499
- );
500
- // Result type: Promise<User | SideEffect>
501
-
502
- // You must continue with pipeAsyncSideEffect, not pipeAsync
503
- const extendedAsyncPipeline = pipeAsyncSideEffect(
504
- asyncPipeline,
505
- processUser,
506
- saveToDatabase
297
+ const label = ifElse(
298
+ (n: number) => n >= 60,
299
+ from('pass'),
300
+ from('fail')
507
301
  );
508
302
  ```
509
303
 
510
- ### 4. Use stream/* for Large Datasets
304
+ ### Stream pipeline to array
511
305
 
512
- ```typescript
306
+ ```ts
513
307
  import { pipe } from 'fp-pack';
514
- import { filter, map, take, toArray, range } from 'fp-pack/stream';
308
+ import { map, filter, toArray } from 'fp-pack/stream';
515
309
 
516
- // GOOD: Lazy processing
517
- const getFirst100Even = pipe(
518
- filter((n: number) => n % 2 === 0),
519
- take(100),
310
+ const toIds = pipe(
311
+ filter((u: User) => u.active),
312
+ map((u) => u.id),
520
313
  toArray
521
314
  );
522
-
523
- // Stops after finding 100 items (only processes 100, not 1 million)
524
- const result = getFirst100Even(range(1, 1000000));
525
315
  ```
526
316
 
527
- ### 5. Handle Errors with SideEffect
528
-
529
- ```typescript
530
- import { pipeSideEffect, SideEffect, runPipeResult } from 'fp-pack';
531
-
532
- const safeDividePipeline = pipeSideEffect(
533
- (input: { a: number; b: number }) => {
534
- if (input.b === 0) {
535
- return SideEffect.of(() => {
536
- throw new Error('Division by zero');
537
- }, 'DIVISION_ERROR');
538
- }
539
- return input;
540
- },
541
- ({ a, b }) => a / b
542
- );
317
+ ### Data-last inference workaround (when needed)
543
318
 
544
- // runPipeResult must be called OUTSIDE the pipeline
545
- const result = runPipeResult(safeDividePipeline({ a: 10, b: 2 })); // 5
546
- ```
319
+ Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
547
320
 
548
- ### 6. Use Control Flow Functions
321
+ ```ts
322
+ import { pipe, pipeHint, zip } from 'fp-pack';
549
323
 
550
- ```typescript
551
- import { pipe, ifElse, when, cond } from 'fp-pack';
324
+ // wrapper
325
+ const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
552
326
 
553
- // GOOD: Declarative conditionals
554
- const processAge = pipe(
555
- ifElse(
556
- (age: number) => age >= 18,
557
- age => ({ age, status: 'adult' }),
558
- age => ({ age, status: 'minor' })
559
- )
327
+ // pipeHint
328
+ const zipUsers2 = pipe(
329
+ pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
560
330
  );
561
-
562
- // GOOD: Multi-branch with cond
563
- const gradeToLetter = cond([
564
- [(n: number) => n >= 90, () => 'A'],
565
- [(n: number) => n >= 80, () => 'B'],
566
- [(n: number) => n >= 70, () => 'C'],
567
- [() => true, () => 'F']
568
- ]);
569
331
  ```
570
332
 
571
- ### 6.1 Type-Safety Tips (prop/ifElse/cond)
572
-
573
- - `prop` returns `T[K] | undefined`. Use `propOr` (or guard) before array operations.
574
- - `ifElse` expects **functions** for both branches. If you already have a value, wrap it: `() => value` or use `from(value)` for cleaner constant branches.
575
- - Use `from(value)` when you need a unary function that ignores input (handy for `ifElse`/`cond` branches and data-first patterns). Pipelines that start with `from(...)` can be called without an initial input value.
576
- - `cond` returns `R | undefined`. Add a default branch and coalesce when you need a strict result.
577
- - In `pipeSideEffect`, keep step return types aligned to avoid wide unions.
578
-
579
- ```typescript
580
- import { pipe, propOr, append, assoc, ifElse, cond, from, filter, map } from 'fp-pack';
581
-
582
- // propOr keeps the type strict for array ops
583
- const addTodo = (text: string, state: AppState) =>
584
- pipe(
585
- propOr([], 'todos'),
586
- append(createTodo(text)),
587
- (todos) => assoc('todos', todos, state)
588
- )(state);
589
-
590
- // ifElse expects functions, not values
591
- const toggleTodo = (id: string) => ifElse(
592
- (todo: Todo) => todo.id === id,
593
- assoc('completed', true),
594
- (todo) => todo
595
- );
596
-
597
- // from is useful for constant branches - cleaner than () => value
598
- const statusLabel = ifElse(
599
- (score: number) => score >= 60,
600
- from('pass'), // Constant value
601
- from('fail')
602
- );
603
-
604
- // Data-first pattern with from: inject data into pipeline
605
- const processData = pipe(
606
- from([1, 2, 3, 4, 5]),
607
- filter((n: number) => n % 2 === 0),
608
- map(n => n * 2)
609
- );
610
- const result = processData(); // [4, 8]
611
-
612
- // cond still returns R | undefined, so coalesce if needed
613
- const grade = (score: number) =>
614
- cond([
615
- [(n: number) => n >= 90, () => 'A'],
616
- [() => true, () => 'F']
617
- ])(score) ?? 'F';
618
- ```
333
+ ### Async pipelines (retry/timeout)
619
334
 
620
- ### 7. Object Transformations
335
+ Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
621
336
 
622
- ```typescript
623
- import { pipe, pick, mapValues, merge } from 'fp-pack';
337
+ ```ts
338
+ import { pipeAsync, retry, timeout } from 'fp-pack';
624
339
 
625
- // GOOD: Declarative object operations
626
- const processUser = pipe(
627
- pick(['name', 'email', 'age']),
628
- mapValues((value) => typeof value === 'string' ? value.trim() : value),
629
- merge({ verified: false })
340
+ const fetchJson = pipeAsync(
341
+ (url: string) => fetch(url),
342
+ (res) => res.json()
630
343
  );
631
- ```
632
-
633
- ## Anti-Patterns to Avoid
634
344
 
635
- ### Don't use imperative loops
636
-
637
- ```typescript
638
- // BAD
639
- const result = [];
640
- for (const item of items) {
641
- if (item.active) {
642
- result.push(item.name);
643
- }
644
- }
645
-
646
- // GOOD
647
- const result = pipe(
648
- filter((item: Item) => item.active),
649
- map(item => item.name)
650
- )(items);
345
+ const fetchJsonWithGuards = pipeAsync(
346
+ fetchJson,
347
+ timeout(5_000),
348
+ retry(3)
349
+ );
651
350
  ```
652
351
 
653
- ### Don't chain array methods
352
+ ### Object updates (avoid mutation)
654
353
 
655
- ```typescript
656
- // BAD
657
- const result = users
658
- .filter(u => u.active)
659
- .map(u => u.name)
660
- .slice(0, 10);
354
+ ```ts
355
+ import { pipe, assocPath, merge } from 'fp-pack';
661
356
 
662
- // GOOD
663
- const result = pipe(
664
- filter((u: User) => u.active),
665
- map(u => u.name),
666
- take(10)
667
- )(users);
668
- ```
669
-
670
- ### ❌ Don't use traditional monads (Option, Either, Maybe)
671
-
672
- ```typescript
673
- // BAD - Don't implement this pattern
674
- const maybeUser = Option.of(user)
675
- .map(u => u.profile)
676
- .flatMap(p => p.email);
677
-
678
- // GOOD - Use SideEffect with pipeSideEffect
679
- const getUserEmail = pipeSideEffect(
680
- (user: User) => {
681
- if (!user.profile) {
682
- return SideEffect.of(() => null, 'NO_PROFILE');
683
- }
684
- return user.profile;
685
- },
686
- (profile) => {
687
- if (!profile.email) {
688
- return SideEffect.of(() => null, 'NO_EMAIL');
689
- }
690
- return profile.email;
691
- }
357
+ const updateAccount = pipe(
358
+ assocPath(['profile', 'role'], 'member'),
359
+ merge({ updatedAt: Date.now() })
692
360
  );
693
361
  ```
694
362
 
695
- ### ❌ Don't mutate data
363
+ ---
696
364
 
697
- ```typescript
698
- // BAD
699
- const updateUser = (user: User) => {
700
- user.lastLogin = new Date();
701
- return user;
702
- };
365
+ ## Decision Guide (What Should the Agent Pick?)
703
366
 
704
- // GOOD
705
- const updateUser = (user: User) => ({
706
- ...user,
707
- lastLogin: new Date()
708
- });
367
+ - Is everything sync and pure? → `pipe`
368
+ - Any step async? `pipeAsync`
369
+ - Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
370
+ - Need early-exit but type precision doesn’t matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
371
+ - Handling result at boundary?
372
+ - Need exact branch types? → `isSideEffect` + separate branches
373
+ - Just want to “unwrap” and don’t care about precision? → `runPipeResult` (provide generics if widened)
374
+ - Large/unbounded/iterable data? → `fp-pack/stream`
709
375
 
710
- // EVEN BETTER with fp-pack
711
- import { assoc } from 'fp-pack';
712
- const updateUser = assoc('lastLogin', new Date());
713
- ```
376
+ ---
714
377
 
715
378
  ## Quick Reference
716
379
 
717
380
  ### Import Paths
718
- - Main functions: `import { pipe, map, filter } from 'fp-pack'`
719
- - Async: `import { pipeAsync, delay, retry } from 'fp-pack'`
720
- - SideEffect: `import { pipeSideEffect, pipeSideEffectStrict, pipeAsyncSideEffect, pipeAsyncSideEffectStrict, SideEffect } from 'fp-pack'`
381
+
382
+ - Main: `import { pipe, map, filter } from 'fp-pack'`
383
+ - SideEffect pipes: `import { pipeSideEffect, pipeSideEffectStrict, SideEffect } from 'fp-pack'`
384
+ - Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
721
385
  - Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
722
386
 
723
387
  ### When to Use What
724
- - **Pure sync transformations**: `pipe` + array/object functions
725
- - **Pure async operations**: `pipeAsync`
726
- - **Error handling with SideEffect**: `pipeSideEffect` (sync) / `pipeAsyncSideEffect` (async)
727
- - **Strict SideEffect unions**: `pipeSideEffectStrict` (sync) / `pipeAsyncSideEffectStrict` (async)
728
- - **Type-safe result handling**: `isSideEffect` for precise type narrowing (prefer this when you need branch-specific types)
729
- - **Execute SideEffect**: `runPipeResult` (call OUTSIDE pipelines). If the input is narrowed to `SideEffect<R>`, it returns `R`. If the input is widened to `SideEffect<any>`/`any`, the result becomes `any`; provide generics to recover
730
- - **Large datasets**: `stream/*` functions
731
- - **Conditionals**: `ifElse`, `when`, `unless`, `cond`
732
- - **Object access**: `prop`, `propStrict`, `path`, `pick`, `omit`
733
- - **Object updates**: `assoc`, `merge`, `evolve`
734
-
735
- ## UI Framework Integration Patterns
736
-
737
- fp-pack works seamlessly with UI frameworks. Here are common patterns organized by **use case**, not framework.
738
-
739
- ### Pattern 1: Handling User Input
740
-
741
- **When**: Form inputs, button clicks, drag & drop, any user interaction
742
- **Where to use**: Event handlers (onChange, @input, on:click, etc.)
743
-
744
- ```typescript
745
- import { pipe, pipeAsyncSideEffect, trim, prop, assoc, tap, SideEffect, runPipeResult } from 'fp-pack';
746
-
747
- // GOOD: Process form input declaratively
748
- const handleNameChange = pipe(
749
- prop('currentTarget'), // Safer than target in most UI libs
750
- (el) => (el as HTMLInputElement).value,
751
- trim,
752
- tap((value) => {
753
- // Prefer updater form to avoid stale state in React-like frameworks
754
- setFormState(prev => assoc('name', value, prev));
755
- })
756
- );
757
-
758
- // Use in any framework:
759
- // React: <input onChange={handleNameChange} />
760
- // Vue: <input @input="handleNameChange" />
761
- // Svelte: <input on:input={handleNameChange} />
762
-
763
- // GOOD: Complex form validation
764
- const validateFieldsOrStop = (data: any) => {
765
- const errors = validateFields(data);
766
- if (!errors) return data;
767
- return SideEffect.of(() => {
768
- setErrors(errors);
769
- return null;
770
- }, 'VALIDATION_ERROR');
771
- };
772
-
773
- const handleSubmitPipeline = pipeAsyncSideEffect(
774
- tap((e: Event) => e.preventDefault()),
775
- prop('currentTarget'),
776
- (form) => getFormData(form as HTMLFormElement),
777
- validateFieldsOrStop, // Returns data or SideEffect
778
- sanitizeInput,
779
- submitToAPI
780
- );
781
-
782
- const handleSubmit = (e: Event) => runPipeResult(handleSubmitPipeline(e));
783
- ```
784
-
785
- ### Pattern 2: Computing Derived/Reactive Values
786
388
 
787
- **When**: Displaying filtered/sorted/transformed data from state
788
- **Where to use**: Computed properties, memoized values, derived state
389
+ - Pure sync transforms: `pipe`
390
+ - Pure async transforms: `pipeAsync`
391
+ - Early-exit pipelines: `pipeSideEffect*` / `pipeAsyncSideEffect*`
392
+ - Strict effect unions: prefer `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
393
+ - Runtime branching: prefer `isSideEffect`
394
+ - Boundary unwrapping: `runPipeResult` (provide generics if inference is widened)
395
+ - Large/unbounded data: `fp-pack/stream`
789
396
 
790
- ```typescript
791
- import { pipe, filter, sortBy, map, take } from 'fp-pack';
792
-
793
- // GOOD: Create reusable data transformation
794
- const processUsers = pipe(
795
- filter((u: User) => u.status === 'active'),
796
- sortBy(u => u.lastLogin),
797
- map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` })),
798
- take(50)
799
- );
800
-
801
- // Use in any framework:
802
- // React: const processed = useMemo(() => processUsers(users), [users]);
803
- // Vue: const processed = computed(() => processUsers(users.value));
804
- // Svelte: $: processed = processUsers($users);
805
- // Solid: const processed = createMemo(() => processUsers(users()));
806
-
807
- // GOOD: Search + filter + pagination
808
- const searchUsers = (query: string, page: number) =>
809
- pipe(
810
- filter((u: User) =>
811
- u.name.toLowerCase().includes(query.toLowerCase())
812
- ),
813
- sortBy(u => u.name),
814
- chunk(20), // Paginate
815
- (pages) => pages[page] || []
816
- );
817
-
818
- // React example:
819
- // const results = useMemo(
820
- // () => searchUsers(searchQuery, currentPage)(allUsers),
821
- // [searchQuery, currentPage, allUsers]
822
- // );
823
- ```
824
-
825
- ### Pattern 3: Async Data Fetching and Processing
826
-
827
- **When**: API calls, database queries, file operations
828
- **Where to use**: Lifecycle hooks, effects, async event handlers
829
-
830
- ```typescript
831
- import { pipeAsync, pipeAsyncSideEffect, tap, SideEffect, runPipeResult } from 'fp-pack';
832
- import { filter, map } from 'fp-pack';
833
-
834
- // GOOD: Fetch + transform + update state
835
- const fetchAndProcessUsers = pipeAsync(
836
- async (userId: string) => fetch(`/api/users/${userId}/friends`),
837
- async (res) => res.json(),
838
- filter((u: User) => u.isActive),
839
- map(u => ({ id: u.id, name: u.name, avatar: u.avatar })),
840
- (processed) => {
841
- setUsers(processed); // Framework-specific state update
842
- return processed;
843
- }
844
- );
845
-
846
- // Use in any framework:
847
- // React: useEffect(() => { fetchAndProcessUsers(id); }, [id]);
848
- // Vue: watchEffect(() => fetchAndProcessUsers(userId.value));
849
- // Svelte: $: fetchAndProcessUsers($userId);
850
-
851
- // GOOD: Error handling with SideEffect
852
- const validateResponseOrStop = (users: unknown) => {
853
- if (!Array.isArray(users)) {
854
- return SideEffect.of(() => {
855
- setError('Invalid response');
856
- return [];
857
- }, 'INVALID_RESPONSE');
858
- }
859
- return users as User[];
860
- };
861
-
862
- const safeFetchUsersPipeline = pipeAsyncSideEffect(
863
- fetchUsers,
864
- validateResponseOrStop,
865
- filter((u: User) => u.verified),
866
- tap((users) => setUsers(users))
867
- );
868
-
869
- const safeFetchUsers = () => runPipeResult(safeFetchUsersPipeline());
870
- ```
871
-
872
- ### Pattern 4: List/Table Data Processing
873
-
874
- **When**: Displaying lists, tables, grids with search/filter/sort
875
- **Where to use**: Component render logic, computed values
876
-
877
- ```typescript
878
- import { pipe, filter, sortBy, groupBy, map } from 'fp-pack';
879
-
880
- // GOOD: Complete table data pipeline
881
- const processTableData = (
882
- data: Product[],
883
- filters: Filters,
884
- sortConfig: SortConfig
885
- ) => pipe(
886
- // Apply filters
887
- filter((p: Product) => {
888
- if (filters.category && p.category !== filters.category) return false;
889
- if (filters.minPrice && p.price < filters.minPrice) return false;
890
- if (filters.maxPrice && p.price > filters.maxPrice) return false;
891
- return true;
892
- }),
893
- // Apply sorting
894
- sortBy(sortConfig.direction === 'asc'
895
- ? (p) => p[sortConfig.key]
896
- : (p) => -p[sortConfig.key]
897
- ),
898
- // Add row metadata
899
- map((product, index) => ({
900
- ...product,
901
- rowId: `row-${index}`,
902
- isEven: index % 2 === 0
903
- }))
904
- )(data);
905
-
906
- // GOOD: Group for categorized display
907
- const groupProductsByCategory = pipe(
908
- groupBy((p: Product) => p.category),
909
- (grouped) => Object.entries(grouped).map(([category, products]) => ({
910
- category,
911
- products,
912
- count: products.length,
913
- totalValue: products.reduce((sum, p) => sum + p.price, 0)
914
- }))
915
- );
916
- ```
917
-
918
- ### Pattern 5: Form State Management
919
-
920
- **When**: Complex forms with validation and state
921
- **Where to use**: Form submission, field updates, validation
922
-
923
- ```typescript
924
- import { pipe, pipeSideEffect, assoc, pick, mapValues, SideEffect, runPipeResult } from 'fp-pack';
925
-
926
- // GOOD: Update nested form state immutably
927
- const updateField = (fieldName: string, value: any) =>
928
- pipe(
929
- assoc(fieldName, value),
930
- (state) => assoc('touched', { ...state.touched, [fieldName]: true }, state)
931
- );
932
-
933
- // GOOD: Form submission with validation
934
- const validateFormOrStop = (data: any) => {
935
- const errors = validateFormData(data);
936
- return Object.keys(errors).length > 0
937
- ? SideEffect.of(() => {
938
- setFormErrors(errors);
939
- return null;
940
- }, 'VALIDATION_ERROR')
941
- : data;
942
- };
943
-
944
- const submitFormPipeline = pipeSideEffect(
945
- pick(['email', 'password', 'name']), // Only include relevant fields
946
- mapValues((v) => typeof v === 'string' ? v.trim() : v), // Sanitize
947
- validateFormOrStop,
948
- submitToAPI
949
- );
950
-
951
- const submitForm = (data: any) => runPipeResult(submitFormPipeline(data));
952
-
953
- // GOOD: Multi-step form state
954
- const validateCurrentStepOrStop = (state: any) => {
955
- const errors = validateCurrentStep(state);
956
- if (!errors) return state;
957
- return SideEffect.of(() => {
958
- setStepErrors(errors);
959
- return state;
960
- }, 'STEP_VALIDATION_ERROR');
961
- };
962
-
963
- const goToNextStepPipeline = pipeSideEffect(
964
- validateCurrentStepOrStop,
965
- (state) => assoc('currentStep', state.currentStep + 1, state)
966
- );
967
-
968
- const goToNextStep = (state: any) => runPipeResult(goToNextStepPipeline(state));
969
- ```
970
-
971
- ### Pattern 6: Real-time Data Streams
972
-
973
- **When**: WebSocket updates, SSE, real-time data
974
- **Where to use**: WebSocket handlers, event listeners
975
-
976
- ```typescript
977
- import { pipe, filter, map, take } from 'fp-pack';
978
-
979
- // GOOD: Process incoming WebSocket messages
980
- const handleWebSocketMessage = pipe(
981
- (event: MessageEvent) => JSON.parse(event.data),
982
- filter((msg: Message) => msg.type === 'USER_UPDATE'),
983
- map(msg => msg.payload),
984
- (update) => {
985
- // Update state with new data
986
- setUsers(prevUsers =>
987
- prevUsers.map(u => u.id === update.id ? { ...u, ...update } : u)
988
- );
989
- }
990
- );
991
-
992
- // websocket.onmessage = handleWebSocketMessage;
993
-
994
- // GOOD: Batch updates with stream
995
- import { pipe as streamPipe, filter as streamFilter, take as streamTake, toArray } from 'fp-pack/stream';
996
- import { pipeAsync, runPipeResult } from 'fp-pack';
997
-
998
- const processBatchUpdates = async (updates: AsyncIterable<Update>) => {
999
- const processed = await streamPipe(
1000
- streamFilter((u: Update) => u.priority === 'high'),
1001
- streamTake(100),
1002
- toArray
1003
- )(updates);
1004
-
1005
- batchUpdateUI(processed);
1006
- };
1007
- ```
1008
-
1009
- ### Pattern 7: Component Props Transformation
1010
-
1011
- **When**: Passing data to child components
1012
- **Where to use**: Component composition, prop drilling
1013
-
1014
- ```typescript
1015
- import { pipe, pick, map, merge } from 'fp-pack';
1016
-
1017
- // GOOD: Transform data for child component
1018
- const prepareUserCardProps = pipe(
1019
- pick(['id', 'name', 'avatar', 'status']),
1020
- merge({
1021
- onClick: handleUserClick,
1022
- className: 'user-card'
1023
- })
1024
- );
1025
-
1026
- // Usage:
1027
- // const userProps = prepareUserCardProps(user);
1028
- // <UserCard {...userProps} />
1029
-
1030
- // GOOD: Prepare list of component props
1031
- const prepareListItems = pipe(
1032
- filter((item: Item) => item.visible),
1033
- map(item => ({
1034
- key: item.id,
1035
- ...pick(['title', 'description', 'icon'], item),
1036
- onClick: () => handleClick(item.id),
1037
- isActive: item.id === activeId
1038
- }))
1039
- );
1040
-
1041
- // Usage:
1042
- // {prepareListItems(items).map(props => <ListItem {...props} />)}
1043
- ```
1044
-
1045
- ### Pattern 8: State Update Reducers
1046
-
1047
- **When**: Complex state updates, global state management
1048
- **Where to use**: Redux/Zustand/Pinia reducers, state update functions
1049
-
1050
- ```typescript
1051
- import { pipe, assoc, dissoc, merge, evolve } from 'fp-pack';
1052
-
1053
- // GOOD: Redux-style reducer with fp-pack
1054
- const userReducer = (state: State, action: Action) => {
1055
- switch (action.type) {
1056
- case 'ADD_USER':
1057
- return pipe(
1058
- prop('users'),
1059
- append(action.payload),
1060
- (users) => assoc('users', users, state)
1061
- )(state);
1062
-
1063
- case 'UPDATE_USER':
1064
- return pipe(
1065
- prop('users'),
1066
- map((u: User) => u.id === action.payload.id
1067
- ? merge(u, action.payload.updates)
1068
- : u
1069
- ),
1070
- (users) => assoc('users', users, state)
1071
- )(state);
1072
-
1073
- case 'DELETE_USER':
1074
- return pipe(
1075
- prop('users'),
1076
- filter((u: User) => u.id !== action.payload),
1077
- (users) => assoc('users', users, state)
1078
- )(state);
1079
-
1080
- default:
1081
- return state;
1082
- }
1083
- };
1084
-
1085
- // GOOD: Using evolve for nested updates
1086
- const updateNestedState = evolve({
1087
- user: evolve({
1088
- profile: merge({ verified: true }),
1089
- settings: assoc('notifications', false)
1090
- }),
1091
- lastUpdated: () => new Date()
1092
- });
1093
- ```
1094
-
1095
- ### Pattern 9: Optimistic Updates
1096
-
1097
- **When**: UI updates before server confirmation
1098
- **Where to use**: Create/update/delete operations
1099
-
1100
- ```typescript
1101
- import { pipe, pipeAsync, append, filter } from 'fp-pack';
1102
-
1103
- // GOOD: Optimistic create with rollback
1104
- const createItemOptimistically = (newItem: Item) => {
1105
- const tempId = `temp-${Date.now()}`;
1106
- const optimisticItem = { ...newItem, id: tempId, pending: true };
1107
-
1108
- // Immediately update UI
1109
- setItems(pipe(append(optimisticItem)));
1110
-
1111
- // Then persist
1112
- return pipeAsync(
1113
- async () => api.createItem(newItem),
1114
- (savedItem) => {
1115
- // Replace temp with real item
1116
- setItems(
1117
- pipe(
1118
- filter((item: Item) => item.id !== tempId),
1119
- append(savedItem)
1120
- )
1121
- );
1122
- return savedItem;
1123
- }
1124
- )().catch((error) => {
1125
- // Rollback on error
1126
- setItems(pipe(filter((item: Item) => item.id !== tempId)));
1127
- throw error;
1128
- });
1129
- };
1130
- ```
1131
-
1132
- ### Pattern 10: URL/Query Parameter Handling
1133
-
1134
- **When**: Syncing UI state with URL
1135
- **Where to use**: Routing, search parameters, filters
1136
-
1137
- ```typescript
1138
- import { pipe, pick, mapValues, merge } from 'fp-pack';
1139
-
1140
- // GOOD: Parse query params to state
1141
- const parseQueryParams = pipe(
1142
- (search: string) => new URLSearchParams(search),
1143
- (params) => ({
1144
- page: Number(params.get('page')) || 1,
1145
- query: params.get('q') || '',
1146
- category: params.get('category') || 'all',
1147
- sort: params.get('sort') || 'date'
1148
- })
1149
- );
1150
-
1151
- // GOOD: Convert state to query params
1152
- const stateToQueryParams = pipe(
1153
- pick(['page', 'query', 'category', 'sort']),
1154
- (state) => {
1155
- const params = new URLSearchParams();
1156
- Object.entries(state).forEach(([key, value]) => {
1157
- if (value) params.set(key, String(value));
1158
- });
1159
- return params.toString();
1160
- }
1161
- );
1162
-
1163
- // Usage in framework router:
1164
- // const filters = parseQueryParams(location.search);
1165
- // navigate(`/products?${stateToQueryParams(currentState)}`);
1166
- ```
1167
-
1168
- ### Pattern 11: Infinite Scroll / Virtual Lists
1169
-
1170
- **When**: Large lists with lazy loading, infinite scroll, virtual rendering
1171
- **Where to use**: Scroll handlers, pagination, large dataset rendering
1172
-
1173
- ```typescript
1174
- import { pipe, pipeAsyncSideEffect, when, tap, runPipeResult } from 'fp-pack';
1175
- import { pipe as streamPipe, filter as streamFilter, take as streamTake, toArray } from 'fp-pack/stream';
1176
-
1177
- // GOOD: Infinite scroll with pipe - all logic inside
1178
- const handleScroll = pipe(
1179
- (e: Event) => e.target as HTMLElement,
1180
- (el) => ({
1181
- scrollTop: el.scrollTop,
1182
- scrollHeight: el.scrollHeight,
1183
- clientHeight: el.clientHeight,
1184
- hasMore,
1185
- isLoading
1186
- }),
1187
- // Only load if near bottom, has more data, and not currently loading
1188
- when(
1189
- ({ scrollTop, scrollHeight, clientHeight, hasMore, isLoading }) =>
1190
- scrollTop + clientHeight >= scrollHeight - 100 && hasMore && !isLoading,
1191
- tap(() => loadNextPage())
1192
- )
1193
- );
1194
-
1195
- // GOOD: Load next page with stream processing
1196
- const loadNextPagePipeline = pipeAsyncSideEffect(
1197
- async () => {
1198
- setIsLoading(true);
1199
- return fetchItemsFromAPI(currentPage);
1200
- },
1201
- // Use stream for lazy processing
1202
- (items) => streamPipe(
1203
- streamFilter((item: Item) => item.visible),
1204
- streamTake(pageSize),
1205
- toArray
1206
- )(items),
1207
- tap((newItems) => setItems(prev => [...prev, ...newItems])),
1208
- tap(() => setCurrentPage(prev => prev + 1)),
1209
- tap(() => setIsLoading(false))
1210
- );
1211
-
1212
- const loadNextPage = () => runPipeResult(loadNextPagePipeline());
1213
-
1214
- // GOOD: Virtual scroll - calculate visible range in pipe
1215
- const getVisibleItems = pipe(
1216
- (scrollTop: number) => ({
1217
- itemHeight: 50,
1218
- viewportHeight: 600,
1219
- bufferSize: 5,
1220
- scrollTop
1221
- }),
1222
- ({ itemHeight, viewportHeight, bufferSize, scrollTop }) => ({
1223
- startIndex: Math.floor(scrollTop / itemHeight),
1224
- endIndex: Math.ceil((scrollTop + viewportHeight) / itemHeight),
1225
- bufferSize
1226
- }),
1227
- ({ startIndex, endIndex, bufferSize }) => ({
1228
- start: Math.max(0, startIndex - bufferSize),
1229
- end: endIndex + bufferSize
1230
- }),
1231
- ({ start, end }) => allItems.slice(start, end)
1232
- );
1233
-
1234
- // GOOD: Lazy load with intersection observer
1235
- const handleIntersection = pipe(
1236
- (entries: IntersectionObserverEntry[]) => entries[0],
1237
- when(
1238
- (entry) => entry.isIntersecting && hasMoreItems && !isLoadingMore,
1239
- tap(() => loadMoreItems())
1240
- )
1241
- );
1242
- ```
1243
-
1244
- ### Pattern 12: Conditional State Updates
1245
-
1246
- **When**: State updates that depend on conditions
1247
- **Where to use**: Any state update with business logic
1248
-
1249
- ```typescript
1250
- import { pipe, ifElse, when, cond, tap, assoc, prop, append, map, filter, merge } from 'fp-pack';
1251
-
1252
- // GOOD: Use ifElse instead of if/else
1253
- const toggleUserStatus = pipe(
1254
- ifElse(
1255
- (user: User) => user.status === 'active',
1256
- assoc('status', 'inactive'),
1257
- assoc('status', 'active')
1258
- ),
1259
- tap((updatedUser) => setUser(updatedUser))
1260
- );
1261
-
1262
- // GOOD: Use ifElse for conditional side effects
1263
- const saveIfValid = pipe(
1264
- validateForm,
1265
- ifElse(
1266
- (result) => result.isValid,
1267
- pipe(
1268
- tap((data) => saveToAPI(data)),
1269
- tap(() => showSuccessMessage())
1270
- ),
1271
- tap((result) => setErrors(result.errors))
1272
- )
1273
- );
1274
-
1275
- // GOOD: Use cond for multi-branch logic (instead of switch/if-else chain)
1276
- const processUserAction = pipe(
1277
- prop('action'),
1278
- cond([
1279
- [
1280
- (action) => action.type === 'CREATE',
1281
- pipe(
1282
- prop('payload'),
1283
- (user) => append(user),
1284
- tap((users) => setUsers(users))
1285
- )
1286
- ],
1287
- [
1288
- (action) => action.type === 'UPDATE',
1289
- pipe(
1290
- prop('payload'),
1291
- ({ id, updates }) => map((u: User) => u.id === id ? merge(u, updates) : u),
1292
- tap((users) => setUsers(users))
1293
- )
1294
- ],
1295
- [
1296
- (action) => action.type === 'DELETE',
1297
- pipe(
1298
- prop('payload'),
1299
- (id) => filter((u: User) => u.id !== id),
1300
- tap((users) => setUsers(users))
1301
- )
1302
- ],
1303
- [
1304
- () => true, // default case
1305
- tap(() => console.warn('Unknown action'))
1306
- ]
1307
- ])
1308
- );
1309
-
1310
- // GOOD: Complex state update with all logic in pipe
1311
- const updateCartItem = (itemId: string, quantity: number) => pipe(
1312
- // Get current cart
1313
- (cart) => cart.items,
1314
- // Find and update item
1315
- map((item: CartItem) =>
1316
- ifElse(
1317
- () => item.id === itemId,
1318
- pipe(
1319
- assoc('quantity', quantity),
1320
- when(
1321
- (updated) => updated.quantity <= 0,
1322
- () => null // Mark for removal
1323
- )
1324
- ),
1325
- () => item
1326
- )(item)
1327
- ),
1328
- // Remove null items (quantity <= 0)
1329
- filter((item) => item !== null),
1330
- // Update state
1331
- tap((items) => setCart({ items })),
1332
- // Show notification
1333
- tap((items) => {
1334
- const item = items.find(i => i.id === itemId);
1335
- if (item) showNotification(`Updated ${item.name}`);
1336
- else showNotification('Item removed from cart');
1337
- })
1338
- );
1339
- ```
1340
-
1341
- ## Library Integration Quick Reference
1342
-
1343
- This section shows how to integrate fp-pack with popular UI libraries. All examples keep logic **inside pipe chains** using fp-pack control flow functions.
1344
-
1345
- ### React Ecosystem
1346
-
1347
- #### React Query / TanStack Query
1348
-
1349
- ```typescript
1350
- import { useQuery, useMutation } from '@tanstack/react-query';
1351
- import { pipe, filter, sortBy, map, tap, when } from 'fp-pack';
1352
-
1353
- // GOOD: Transform data in select using pipe
1354
- const { data: activeUsers } = useQuery({
1355
- queryKey: ['users'],
1356
- queryFn: fetchUsers,
1357
- select: pipe(
1358
- filter((u: User) => u.status === 'active'),
1359
- sortBy((u) => u.name),
1360
- map((u) => ({ id: u.id, name: u.name, email: u.email }))
1361
- )
1362
- });
1363
-
1364
- // GOOD: Handle mutations with pipe
1365
- const mutation = useMutation({
1366
- mutationFn: createUser,
1367
- onSuccess: pipe(
1368
- tap((newUser) => queryClient.invalidateQueries(['users'])),
1369
- when(
1370
- (user) => user.isPremium,
1371
- tap(() => showPremiumWelcome())
1372
- ),
1373
- tap(() => navigate('/dashboard'))
1374
- )
1375
- });
1376
-
1377
- // GOOD: Optimistic updates in pipe
1378
- const updateMutation = useMutation({
1379
- mutationFn: updateUser,
1380
- onMutate: pipe(
1381
- tap(async (newUser) => {
1382
- await queryClient.cancelQueries(['users']);
1383
- const previous = queryClient.getQueryData(['users']);
1384
- queryClient.setQueryData(['users'], pipe(
1385
- map((u: User) => u.id === newUser.id ? merge(u, newUser) : u)
1386
- ));
1387
- return { previous };
1388
- })
1389
- ),
1390
- onError: pipe(
1391
- tap((err, variables, context) => {
1392
- if (context?.previous) {
1393
- queryClient.setQueryData(['users'], context.previous);
1394
- }
1395
- })
1396
- )
1397
- });
1398
- ```
1399
-
1400
- #### Zustand
1401
-
1402
- ```typescript
1403
- import create from 'zustand';
1404
- import { pipe, append, filter, map, merge, ifElse, when, tap, prop, sortBy, assoc } from 'fp-pack';
1405
-
1406
- // GOOD: All actions use pipe
1407
- const useStore = create((set, get) => ({
1408
- users: [],
1409
-
1410
- addUser: pipe(
1411
- (user: User) => user,
1412
- when(
1413
- (user) => !get().users.some(u => u.id === user.id),
1414
- tap((user) => set(pipe(
1415
- prop('users'),
1416
- append(user),
1417
- sortBy((u: User) => u.name),
1418
- (users) => ({ users })
1419
- )(get())))
1420
- )
1421
- ),
1422
-
1423
- updateUser: (id: string, updates: Partial<User>) => set(pipe(
1424
- prop('users'),
1425
- map((u: User) =>
1426
- ifElse(
1427
- () => u.id === id,
1428
- merge(u, updates),
1429
- () => u
1430
- )(u)
1431
- ),
1432
- (users) => ({ users })
1433
- )(get())),
1434
-
1435
- deleteUser: (id: string) => set(pipe(
1436
- prop('users'),
1437
- filter((u: User) => u.id !== id),
1438
- (users) => ({ users })
1439
- )(get())),
1440
-
1441
- toggleUserStatus: (id: string) => set(pipe(
1442
- prop('users'),
1443
- map((u: User) => u.id === id
1444
- ? pipe(
1445
- ifElse(
1446
- (user) => user.status === 'active',
1447
- assoc('status', 'inactive'),
1448
- assoc('status', 'active')
1449
- )
1450
- )(u)
1451
- : u
1452
- ),
1453
- (users) => ({ users })
1454
- )(get()))
1455
- }));
1456
- ```
1457
-
1458
- #### Redux Toolkit
1459
-
1460
- ```typescript
1461
- import { createSlice } from '@reduxjs/toolkit';
1462
- import { pipe, append, filter, map, merge, sortBy, cond, assoc } from 'fp-pack';
1463
-
1464
- // GOOD: Reducers with pipe - no manual mutations
1465
- const userSlice = createSlice({
1466
- name: 'users',
1467
- initialState: { list: [], loading: false },
1468
- reducers: {
1469
- addUser: (state, action) => {
1470
- state.list = pipe(
1471
- append(action.payload),
1472
- sortBy((u: User) => u.createdAt)
1473
- )(state.list);
1474
- },
1475
-
1476
- updateUser: (state, action) => {
1477
- state.list = pipe(
1478
- map((u: User) =>
1479
- u.id === action.payload.id
1480
- ? merge(u, action.payload.updates)
1481
- : u
1482
- )
1483
- )(state.list);
1484
- },
1485
-
1486
- deleteUser: (state, action) => {
1487
- state.list = pipe(
1488
- filter((u: User) => u.id !== action.payload)
1489
- )(state.list);
1490
- },
1491
-
1492
- // Complex update with cond
1493
- processAction: (state, action) => {
1494
- state.list = pipe(
1495
- cond([
1496
- [
1497
- () => action.type === 'BULK_ACTIVATE',
1498
- map((u: User) => assoc('status', 'active', u))
1499
- ],
1500
- [
1501
- () => action.type === 'BULK_DELETE',
1502
- filter((u: User) => !action.payload.ids.includes(u.id))
1503
- ],
1504
- [
1505
- () => true,
1506
- (users) => users // no change
1507
- ]
1508
- ])
1509
- )(state.list);
1510
- }
1511
- }
1512
- });
1513
- ```
1514
-
1515
- #### React Hook Form
1516
-
1517
- ```typescript
1518
- import { useForm } from 'react-hook-form';
1519
- import { pipe, pipeSideEffect, pipeAsyncSideEffect, pick, mapValues, trim, when, tap, SideEffect, runPipeResult } from 'fp-pack';
1520
-
1521
- // GOOD: Validation with pipeSideEffect
1522
- const validateFormDataPipeline = pipeSideEffect(
1523
- pick(['email', 'password', 'name']),
1524
- mapValues((v) => typeof v === 'string' ? trim(v) : v),
1525
- (data) => {
1526
- const errors: any = {};
1527
- if (!data.email?.includes('@')) errors.email = 'Invalid email';
1528
- if ((data.password?.length || 0) < 8) errors.password = 'Too short';
1529
- return Object.keys(errors).length > 0
1530
- ? SideEffect.of(() => ({ values: {}, errors }), 'VALIDATION_ERROR')
1531
- : { values: data, errors: {} };
1532
- }
1533
- );
1534
-
1535
- const validateFormData = (values: any) => runPipeResult(validateFormDataPipeline(values));
1536
-
1537
- const { register, handleSubmit } = useForm({
1538
- resolver: (values) => validateFormData(values)
1539
- });
1540
-
1541
- // GOOD: Submit handler with pipeAsyncSideEffect
1542
- const onSubmitPipeline = pipeAsyncSideEffect(
1543
- validateFormData,
1544
- when(
1545
- (result) => Object.keys(result.errors).length === 0,
1546
- pipe(
1547
- prop('values'),
1548
- submitToAPI,
1549
- tap(() => navigate('/success'))
1550
- )
1551
- )
1552
- );
1553
-
1554
- const onSubmit = (data: any) => runPipeResult(onSubmitPipeline(data));
1555
- ```
397
+ ---
1556
398
 
1557
- ### Vue Ecosystem
1558
-
1559
- #### Pinia
1560
-
1561
- ```typescript
1562
- import { defineStore } from 'pinia';
1563
- import { pipe, append, filter, map, merge, sortBy, when, tap } from 'fp-pack';
1564
-
1565
- // GOOD: All actions use pipe
1566
- export const useUserStore = defineStore('user', {
1567
- state: () => ({ users: [], loading: false }),
1568
-
1569
- actions: {
1570
- addUser(user: User) {
1571
- this.users = pipe(
1572
- append(user),
1573
- sortBy((u: User) => u.name),
1574
- when(
1575
- (users) => users.length > 100,
1576
- tap(() => this.showWarning('Many users'))
1577
- )
1578
- )(this.users);
1579
- },
1580
-
1581
- updateUser(id: string, updates: Partial<User>) {
1582
- this.users = pipe(
1583
- map((u: User) => u.id === id ? merge(u, updates) : u)
1584
- )(this.users);
1585
- },
1586
-
1587
- deleteUser(id: string) {
1588
- this.users = pipe(
1589
- filter((u: User) => u.id !== id),
1590
- tap((users) => {
1591
- if (users.length === 0) this.showEmptyState = true;
1592
- })
1593
- )(this.users);
1594
- }
1595
- }
1596
- });
1597
- ```
399
+ ## Anti-Patterns (Avoid)
1598
400
 
1599
- #### VueUse
1600
-
1601
- ```typescript
1602
- import { useFetch } from '@vueuse/core';
1603
- import { pipe, filter, map, sortBy, tap } from 'fp-pack';
1604
-
1605
- // GOOD: Transform response with pipe
1606
- const { data } = useFetch('/api/users', {
1607
- afterFetch: pipe(
1608
- prop('data'),
1609
- filter((u: User) => u.verified),
1610
- sortBy((u) => u.name),
1611
- map((u) => ({ id: u.id, name: u.name })),
1612
- tap((users) => console.log(`Loaded ${users.length} users`))
1613
- )
1614
- }).json();
1615
-
1616
- // GOOD: Refetch with condition in pipe
1617
- const { execute } = useFetch('/api/users');
1618
-
1619
- const refreshIfNeeded = pipe(
1620
- (lastUpdate: Date) => Date.now() - lastUpdate.getTime(),
1621
- when(
1622
- (diff) => diff > 5 * 60 * 1000, // 5 minutes
1623
- tap(() => execute())
1624
- )
1625
- );
1626
- ```
401
+ - Imperative loops when a pipeline is clearer
402
+ - Mutating inputs (prefer immutable transforms)
403
+ - Chaining `Array.prototype.*` in complex transforms (prefer `pipe`)
404
+ - Calling `runPipeResult` inside a SideEffect-aware pipeline
405
+ - Adding “classic monads” to emulate Option/Either instead of using `SideEffect` pipes when necessary
1627
406
 
1628
- ### State Management Patterns
1629
-
1630
- All state management libraries benefit from fp-pack's immutable update patterns:
1631
-
1632
- ```typescript
1633
- // GOOD: Generic state update pattern (works with any library)
1634
- const updateState = <T>(
1635
- state: T,
1636
- path: string[],
1637
- updater: (value: any) => any
1638
- ) => pipe(
1639
- pathOr(null, path),
1640
- updater,
1641
- (newValue) => assocPath(path, newValue, state)
1642
- )(state);
1643
-
1644
- // Usage in any framework:
1645
- // Redux: return updateState(state, ['users', 0, 'name'], toUpper);
1646
- // Zustand: set(updateState(get(), ['users', 0, 'name'], toUpper));
1647
- // Pinia: this.$state = updateState(this.$state, ['users', 0, 'name'], toUpper);
1648
- ```
407
+ ---
1649
408
 
1650
- ## Framework-Specific Notes
409
+ ## Quick Signature Lookup (When Unsure)
1651
410
 
1652
- While the patterns above are framework-agnostic, here's where to apply them:
411
+ Prefer local types first:
412
+ - Main types: `dist/index.d.ts`
413
+ - Stream types: `dist/stream/index.d.ts`
1653
414
 
1654
- ### Reactive/Computed Values
1655
- - **React**: `useMemo(() => pipe(...)(data), [data])`
1656
- - **Vue**: `computed(() => pipe(...)(data.value))`
1657
- - **Svelte**: `$: result = pipe(...)(data)`
1658
- - **Solid**: `createMemo(() => pipe(...)(data()))`
415
+ If you’re in a consumer project:
416
+ - `node_modules/fp-pack/dist/index.d.ts`
417
+ - `node_modules/fp-pack/dist/stream/index.d.ts`
1659
418
 
1660
- ### Event Handlers
1661
- - **React**: `<button onClick={pipe(...)}>` or `const handler = pipe(...)`
1662
- - **Vue**: `<button @click="pipe(...)">` or `const handler = pipe(...)`
1663
- - **Svelte**: `<button on:click={pipe(...)}>` or `const handler = pipe(...)`
419
+ ---
1664
420
 
1665
- ### Side Effects (API calls, subscriptions)
1666
- - **React**: `useEffect(() => { pipeAsync(...)() }, [deps])`
1667
- - **Vue**: `watchEffect(() => pipeAsync(...)())`
1668
- - **Svelte**: `onMount(() => pipeAsync(...)())`
421
+ ## Writing New Helpers (If Needed)
1669
422
 
1670
- ### State Updates
1671
- - **React**: `setState(pipe(...)(currentState))`
1672
- - **Vue**: `state.value = pipe(...)(state.value)`
1673
- - **Svelte**: `$state = pipe(...)($state)`
423
+ If you add your own utilities that should compose well:
424
+ - Keep them **unary** for pipelines (or return a unary function via currying).
425
+ - Prefer **data-last** argument order.
426
+ - For generic/overloaded helpers, consider providing an explicit type alias to preserve inference in TypeScript.
1674
427
 
1675
- All patterns use the same fp-pack functions - only the framework's state/reactive wrapper changes.
428
+ ---
1676
429
 
1677
430
  ## Summary
1678
431
 
1679
- As an AI coding assistant working with fp-pack:
1680
-
1681
- 1. **Default to `pipe`** for all data transformations
1682
- 2. **Switch to `pipeAsync`** when async operations are involved (use `pipeAsyncSideEffect` if SideEffect is in the flow)
1683
- 3. **Use `stream/*`** for lazy, memory-efficient processing
1684
- 4. **Handle errors with `SideEffect`** in `pipeSideEffect`/`pipeAsyncSideEffect`, not try-catch
1685
- 5. **Avoid imperative loops** - use fp-pack's declarative functions
1686
- 6. **Never suggest monads** - use SideEffect pattern instead
1687
- 7. **Keep code declarative** - describe what, not how
1688
- 8. **All logic inside pipe** - never break out of pipe chains for conditionals or loops
1689
- 9. **Use control flow functions** - `when`, `unless`, `ifElse`, `cond` instead of if/else/switch
1690
- 10. **Call `runPipeResult` OUTSIDE pipelines** - `runPipeResult` / `matchSideEffect` must be called outside `pipeSideEffect`/`pipeAsyncSideEffect` for proper type safety
1691
- 11. **Use `isSideEffect` for type narrowing** - get precise types in both success and error branches
1692
- 12. **Apply use-case patterns** - recognize scenarios (form handling, list processing, etc.) and apply appropriate fp-pack patterns
1693
- 13. **Framework-agnostic core** - write fp-pack logic independent of UI framework, only wrap at the boundaries
1694
- 14. **Library integration** - use pipe in select/resolver/action functions of popular libraries (React Query, Zustand, Pinia, etc.)
1695
-
1696
- ### Key Principles
1697
-
1698
- **✅ DO: Keep everything in pipe**
1699
- ```typescript
1700
- // GOOD: All logic inside pipe
1701
- const handleSubmitPipeline = pipeAsyncSideEffect(
1702
- getFormData,
1703
- validateFields,
1704
- when(isValid, submitToAPI),
1705
- unless(isValid, showErrors)
1706
- );
1707
-
1708
- const handleSubmit = (form: HTMLFormElement) => runPipeResult(handleSubmitPipeline(form));
1709
- ```
1710
-
1711
- **❌ DON'T: Break out of pipe for conditionals**
1712
- ```typescript
1713
- // BAD: Breaking pipe for if/else
1714
- const handleSubmit = pipe(
1715
- getFormData,
1716
- validateFields
1717
- );
1718
- const result = handleSubmit(form);
1719
- if (result.isValid) { // ❌ Outside pipe
1720
- submitToAPI(result);
1721
- } else {
1722
- showErrors(result.errors);
1723
- }
1724
- ```
1725
-
1726
- **✅ DO: Use when/cond/ifElse for branching**
1727
- ```typescript
1728
- // GOOD: Branching inside pipe
1729
- const processAction = pipe(
1730
- cond([
1731
- [(action) => action.type === 'CREATE', handleCreate],
1732
- [(action) => action.type === 'UPDATE', handleUpdate],
1733
- [(action) => action.type === 'DELETE', handleDelete],
1734
- [() => true, handleDefault]
1735
- ])
1736
- );
1737
- ```
1738
-
1739
- **❌ DON'T: Use switch/if-else chains**
1740
- ```typescript
1741
- // BAD: Imperative branching
1742
- const processAction = (action) => {
1743
- switch (action.type) { // ❌ Imperative
1744
- case 'CREATE': return handleCreate(action);
1745
- case 'UPDATE': return handleUpdate(action);
1746
- case 'DELETE': return handleDelete(action);
1747
- default: return handleDefault(action);
1748
- }
1749
- };
1750
- ```
1751
-
1752
- Your goal is to write clean, readable, functional code that leverages fp-pack's full potential in real-world UI applications.
432
+ Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary (with generics if inference widens to `any`).