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