name: fp-pack description: Use when working in projects that use fp-pack; follow pipe, SideEffect, and curry guidelines. metadata: short-description: fp-pack workflow version: {{version}}
fp-pack AI Agent Skills
Document Version: {{version}}
Additional materials (optional):
- constraints/ (rules, mistakes, troubleshooting)
- reference/ (composition, currying, TypeScript inference)
- examples/ (quick examples)
⚠️ Activation Condition (Read First)
These guidelines apply only when fp-pack is installed in the current project.
Before following this document:
- Check
package.jsonforfp-packin dependencies/devDependencies - Check
node_modules/fp-packexists - Check existing code imports from
fp-packorfp-pack/stream
If fp-pack is not installed, use the project's existing conventions. Do not suggest adding fp-pack unless the user asks.
Core Rules (Keep In Memory)
- Use
pipe/pipeAsyncfor 2+ steps; for a single step, call the function directly. - Use
pipeStrict/pipeAsyncStrictwhen you want stricter mismatch detection; otherwise stick topipe/pipeAsync. - Prefer value-first:
pipe(value, ...)/pipeAsync(value, ...)runs immediately and improves inference (the input anchors types). Use functions-first only when you need a reusable pipeline. - If the first arg is a function, it's treated as composition; wrap function values with
from(). - Keep pipeline functions unary; prefer data-last, curried helpers.
map/filterare for arrays/iterables, not single values.- Use
from()only for constants or 0-arg pipelines (including function values you need to pass as data). Otherwise pass data as the first argument. - Use
pipeSideEffect*only when you need early exit; otherwise usepipe/pipeAsync. - Never call
runPipeResult/matchSideEffectinside pipelines; call at boundaries. - Prefer
isSideEffectfor precise narrowing;runPipeResultfor unwrapping (use generics if widened). SideEffectis an instance type: useSideEffect<E>(nottypeof SideEffect).- If TS inference stalls in data-last generics, use
pipeHintor a tiny wrapper. - Use
fp-pack/streamfor large/lazy iterables; array/object utils for small/eager data. - Keep DOM/imperative work at the edge; use fp-pack for data transforms.
- Avoid mutation; return new objects/arrays.
- When unsure, check
dist/index.d.tsordist/stream/index.d.ts.
Note: For trivial one-liners, using native JS directly is fine. Reach for fp-pack when composition adds clarity or reuse. Keep pipelines short and readable.
Common Mistakes & Fixes (Top Issues)
Don't wrap data in zero-arg functions
// ❌ BAD
pipe(() => [1, 2, 3], filter((n: number) => n % 2 === 0));
// ✅ GOOD (value-first)
pipe([1, 2, 3], filter((n: number) => n % 2 === 0));
// ✅ GOOD (no-input pipeline)
pipe(from([1, 2, 3]), filter((n: number) => n % 2 === 0))();
ifElse/cond need total branches
const status = ifElse((n: number) => n > 0, from('ok'), from('fail'));
const label = cond<number, string>([
[(n) => n > 0, () => 'positive'],
[() => true, () => 'non-positive'] // default keeps it total
]);
map is for arrays/iterables (not single values)
const save = pipe(
(s: AppState) => JSON.stringify({ todos: s.todos, nextId: s.nextId }),
tap((json) => localStorage.setItem(STORAGE_KEY, json))
);
save(state);
SideEffect pipelines + runPipeResult belong at the boundary
const pipeline = pipeSideEffect(findUser, (user) => user.email);
const result = runPipeResult(pipeline(input)); // outside the pipeline
DOM APIs are imperative by nature—keep them outside or at the boundary (use tap for final effects).
Troubleshooting Type Errors (Fast Checks)
- Is every step unary? (Pipelines expect one input each.)
- Are you using array/iterable helpers (
map,filter,reduce) on non-arrays? - Did you forget a default case in
cond([() => true, () => ...])? - Are you returning
SideEffectfrom apipepipeline? (UsepipeSideEffect*.) - Are you calling
runPipeResultinside a pipeline? (Move it to the boundary.) - Are you mixing async steps in
pipeinstead ofpipeAsync? - Did a data-last generic fail to infer? (Use
pipeHintor a tiny wrapper.) - Are you using
from()for constants only, not normal input? - Is a DOM/imperative step inside the pipeline? (Move it to the edge or use
tap.) - Check
dist/index.d.ts/dist/stream/index.d.tsfor the expected signature.
If the error persists, reduce the pipeline to the smallest failing step and add types there first.
Quick Examples (3)
Example 1: User Data Processing Pipeline
import { pipe, filter, map, take, sortBy } from 'fp-pack';
const result = pipe(
users,
filter((user: User) => user.active),
sortBy((user) => -user.activityScore),
map((user) => user.name),
take(10)
);
Example 2: API Request with Early Exit
import { pipeAsyncSideEffect, SideEffect, runPipeResult } from 'fp-pack';
const result = runPipeResult(
await pipeAsyncSideEffect(
'user-123',
async (userId: string) => {
const res = await fetch(`/api/users/${userId}`);
return res.ok ? res : SideEffect.of(() => `HTTP ${res.status}`);
},
async (res) => res.json()
)
);
Example 3: Value-first pipeline
import { pipe, filter, map } from 'fp-pack';
const result = pipe(
[1, 2, 3, 4, 5],
filter((n: number) => n % 2 === 0),
map((n) => n * 2)
);
Core Composition
pipe (sync)
import { pipe, filter, map, take } from 'fp-pack';
const result = pipe(
users,
filter((u: User) => u.active),
map((u) => u.name),
take(10)
);
pipeAsync (async)
import { pipeAsync } from 'fp-pack';
const user = await pipeAsync(
userId,
async (id: string) => fetch(`/api/users/${id}`),
async (res) => res.json(),
(data) => data.user
);
Currying & Data-Last
Most multi-arg helpers are data-last and curried. Pair them with value-first pipe(value, ...) to anchor types:
- Good:
map(fn),filter(pred),replace(from, to),assoc('k', v),path(['a','b']) - Single-arg helpers are already unary—just use them directly
TypeScript: Data-last Generic Inference
Some data-last helpers return a generic function whose type is only determined by the final data argument. Prefer value-first pipe(value, ...) so the input anchors generics; use hints when needed.
Quick fix (pipeHint or wrapper)
import { pipe, pipeHint, zip, some } from 'fp-pack';
// Prefer value-first to anchor generics
const values: number[] = [1, 2, 3];
const withValueFirst = pipe(
values,
zip([1, 2, 3]),
some(([a, b]) => a > b)
);
const withPipeHint = pipe(
pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
some(([a, b]) => a > b)
);
If you prefer, a tiny wrapper like (values) => zip([1, 2, 3], values) works too.
Utilities that may need a hint in data-last pipelines:
- Array:
chunk,drop,take,zip - Object:
assoc,assocPath,dissocPath,evolve,mapValues,merge,mergeDeep,omit,path,pick,prop - Async:
timeout - Stream:
chunk,drop,take,zip
SideEffect Pattern (Use Only When Needed)
Most code should use pipe / pipeAsync. Use SideEffect-aware pipes only when you need early termination:
- validation pipelines that should stop early
- recoverable errors you want to model as data
- branching flows where you want to short-circuit
SideEffect-aware pipes
pipeSideEffect/pipeAsyncSideEffect: convenient, but may widen effects toanypipeSideEffectStrict/pipeAsyncSideEffectStrict: preserves strict union effects (recommended)
Key functions
SideEffect.of(effectFn, label?)isSideEffect(value)(type guard)runPipeResult(result)(execute effect or return value; outside pipelines)
Example
import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
const result = pipeSideEffectStrict(
-1,
validate,
(n) => n + 1
); // number | SideEffect<'NEG'>
if (isSideEffect(result)) {
const err = runPipeResult(result); // 'NEG'
} else {
// result is number
}
Type Safety Notes
pipeSideEffect/pipeAsyncSideEffectcan widen effects toanyin complex pipelines.pipeSideEffectStrict/pipeAsyncSideEffectStrictpreserve strict effect unions.runPipeResultreturnsRwhen input isSideEffect<R>, but becomesanyif the input is widened toSideEffect<any>/any.- Prefer
isSideEffectfor precise branch narrowing.
Stream Functions (fp-pack/stream)
Use stream utilities when:
- data is large or unbounded
- you want lazy evaluation
- you want to support
IterableandAsyncIterable
If any input is async, the output is async. Use toAsync to normalize inputs when needed.
import { pipe } from 'fp-pack';
import { range, filter, map, take, toArray } from 'fp-pack/stream';
const result = pipe(
range(Infinity),
filter((n: number) => n % 2 === 0),
map((n) => n * n),
take(100),
toArray
);
Available Functions (Quick Index)
Composition
pipe,pipeStrict,pipeAsync,pipeAsyncStrict- SideEffect-aware:
pipeSideEffect,pipeSideEffectStrict,pipeAsyncSideEffect,pipeAsyncSideEffectStrict - Utilities:
from,tap,tap0,once,memoize,identity,constant,curry,compose - SideEffect helpers:
SideEffect,isSideEffect,matchSideEffect,runPipeResult
Array
- Transforms:
map,filter,flatMap,reduce,scan - Queries:
find,some,every - Slicing:
take,drop,chunk - Ordering:
sort,sortBy,groupBy,uniqBy - Combining:
zip,concat,append,flatten
Object
- Access:
prop,path,propOr,pathOr - Pick/drop:
pick,omit - Updates:
assoc,assocPath,dissocPath - Merge:
merge,mergeDeep - Transforms:
mapValues,evolve
Control Flow
ifElse,when,unless,cond,guard,tryCatch
Async
retry,timeout,delaydebounce*,throttle
Stream (Lazy Iterables)
- Building:
range - Transforms:
map,filter,flatMap,flatten - Slicing:
take,drop,chunk - Queries:
find,some,every,reduce - Combining:
zip,concat - Utilities:
toArray,toAsync
Others
- Math:
add,sub,mul,div,clamp - String:
split,join,replace,trim - Equality:
equals,isNil - Debug:
assert,invariant
Micro-Patterns (Optional)
Boundary handling
const pipeline = pipeSideEffectStrict(validate, process);
export const handler = (data) => {
const result = pipeline(data);
if (isSideEffect(result)) return runPipeResult(result);
return result;
};
Value-first execution
const result = pipe(
[1, 2, 3, 4, 5],
filter((n: number) => n % 2 === 0),
map((n) => n * 10)
); // [20, 40]
from() for constants / 0-arg pipelines
const result = pipe(
from([1, 2, 3, 4, 5]),
filter((n: number) => n % 2 === 0),
map((n) => n * 10)
)(); // [20, 40]
Stream to array
const toIds = pipe(
filter((u: User) => u.active),
map((u) => u.id),
toArray
);
Object updates
const updateAccount = pipe(
assocPath(['profile', 'role'], 'member'),
merge({ updatedAt: Date.now() })
);
Decision Guide
- Is everything sync and pure? →
pipe - Any step async? →
pipeAsync - Need early-exit + typed effect unions? →
pipeSideEffectStrict/pipeAsyncSideEffectStrict - Need early-exit but type precision doesn't matter? →
pipeSideEffect/pipeAsyncSideEffect - Only one step? → call the function directly (no
pipe) - Handling result at boundary? →
isSideEffectfor branching,runPipeResultto unwrap - Large/unbounded/iterable data? →
fp-pack/stream
Import Paths
- Main:
import { pipe, map, filter } from 'fp-pack' - SideEffect:
import { pipeSideEffect, SideEffect } from 'fp-pack' - Async:
import { pipeAsync, retry, timeout } from 'fp-pack' - Stream:
import { map, filter, toArray } from 'fp-pack/stream'
Quick Signature Lookup (When Unsure)
If TypeScript inference is stuck or you need to verify a function signature:
In fp-pack project:
- Main types:
dist/index.d.ts - Stream types:
dist/stream/index.d.ts
In consumer project:
- Main types:
node_modules/fp-pack/dist/index.d.ts - Stream types:
node_modules/fp-pack/dist/stream/index.d.ts
Summary
Default to value-first pipe / pipeAsync for inference, keep helpers data-last and unary, switch to stream/* when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use functions-first only for reusable pipelines. Use isSideEffect for precise narrowing and call runPipeResult only at the boundary.