
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
A practical functional toolkit for JavaScript and TypeScript.
The core philosophy of fp-pack is simple: if you know functions, pipes, and currying, you're ready to use it. It's a pipe-first toolkit for everyday code—approachable, zero-deps, and tree-shakeable.
No complex monads. No wrapper ceremony. Just plain functions that compose naturally, with an optional SideEffect pattern for early exits and error handling.
At a glance:
pipe / pipeAsync.SideEffect.of(), and let the pipeline handle the rest. No monad wrappers required.If you want functional composition without a lot of ceremony, fp-pack is designed to be a practical starting point that still scales to larger codebases.
🔄 Pipe-First Philosophy
Built around pipe and pipeAsync for clean, left-to-right function composition.
⚡ SideEffect Pattern
Handle errors and side effects declaratively in SideEffect-aware pipelines. Use pipeSideEffect / pipeAsyncSideEffect to short-circuit on SideEffect without breaking composition. Focus on business logic, not error plumbing. For strict effect unions, use pipeSideEffectStrict / pipeAsyncSideEffectStrict.
📘 JavaScript & TypeScript Works seamlessly in JavaScript. Written in TypeScript for robust type inference when you need it.
🎯 Practical & Production-Ready Covers the patterns you write every day—data transformation, composition, control flow, and async operations.
🪶 Lightweight & Modular Zero dependencies and tree-shakeable modules.
💧 Lazy Stream Processing
Efficient iterable processing with stream/* functions for memory-conscious operations on large datasets.
Pipe-centric composition
pipe (sync) and pipeAsync (async) are the primary composition tools. All utilities are designed to work seamlessly in pipe chains.
DX-optimized type inference
"Don't let strictness hinder inference." fp-pack's standard pipe prioritizes global type stability over local constraints at connection points. The inference chain, designed without NoInfer, lets TypeScript derive perfect result types at the end of your pipeline—even without manual annotations. This "Global Stability" approach means you write less, TypeScript infers more, and your pipelines just work.
Works best in value-first pipelines where the input anchors generics. Function-first or from-start pipelines may need pipeHint or a small wrapper.
When users inappropriately use explicit type annotations, TypeScript's natural inference benefits are reduced, and in some edge cases intermediate type mismatches may be allowed to keep the pipeline flowing. When you need stricter mismatch detection, use pipeStrict/pipeAsyncStrict; for maximum inference power with minimal friction, stick to pipe/pipeAsync. → Pipe Choice Guide
Pragmatic error handling
The SideEffect pattern handles errors declaratively in pipeSideEffect/pipeAsyncSideEffect and short-circuits on SideEffect, so you can keep normal functions.
For strict union typing across branches, use pipeSideEffectStrict / pipeAsyncSideEffectStrict.
Usage notes:
runPipeResult/matchSideEffect outside the pipeline.isSideEffect, runPipeResult returns the effect type; if widened (e.g. SideEffect<any>), pass generics to recover a safe union.Immutable & Pure by default
Core utilities avoid mutations and side effects. Any exception is explicitly named (e.g. tap, log).
Stream functions
Stream helpers (stream/*) provide lazy evaluation for large datasets.
Curried by design All multi-argument utility functions are curried or behave like curried functions, enabling partial application and point-free style. This design allows elegant composition in pipes without awkward wrapper functions.
// Functions are curried - apply arguments one at a time
const double = map((n: number) => n * 2);
const evenOnly = filter((n: number) => n % 2 === 0);
const result = pipe(
evenOnly, // Partially applied filter
double, // Partially applied map
take(5) // Partially applied take
)([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
// [4, 8, 12, 16, 20]
Hybrid data-first / data-last
pipe/pipeAsync accept either functions-first (returns a reusable function) or value-first (executes immediately). Value-first improves inference because the first value anchors generics. If the first argument is a function, it is treated as composition; wrap function values with from when you need to pass them as data. Use from to inject constants (e.g., in ifElse/cond) or to start a pipeline without input.
import { pipe, ifElse, from, filter, map } from 'fp-pack';
// Use from to return constant values in conditional branches
const getStatusLabel = ifElse(
(score: number) => score >= 60,
from('pass'), // Constant value instead of (score) => 'pass'
from('fail')
);
const result = getStatusLabel(75); // 'pass'
// Start a pipeline without input
const processWithData = pipe(
from([1, 2, 3, 4, 5]),
filter((n: number) => n % 2 === 0),
map(n => n * 2)
);
const processed = processWithData(); // [4, 8]
npm install fp-pack
# or
pnpm add fp-pack
# or
yarn add fp-pack
fp-pack includes an AI agent skills package that helps AI coding assistants (Claude Code, GitHub Copilot, Cursor, etc.) automatically write fp-pack-style functional code.
When you have this skills package in your project, AI assistants will:
pipe/pipeAsync for pure transformations, and pipeSideEffect/pipeAsyncSideEffect when SideEffect is involved (use strict variants when you need strict effect unions)SideEffect pattern instead of try-catchstream/* functions for large datasetsCopy the skills folder to your project's .claude/skills/ directory:
# Unix/macOS/Linux
mkdir -p .claude/skills/fp-pack
cp -R node_modules/fp-pack/dist/skills/fp-pack/* .claude/skills/fp-pack/
# Windows (PowerShell)
New-Item -ItemType Directory -Force -Path .claude/skills/fp-pack
Copy-Item node_modules/fp-pack/dist/skills/fp-pack/* .claude/skills/fp-pack -Recurse
# Or manually create the directory and copy
mkdir -p .claude/skills/fp-pack
cp -R node_modules/fp-pack/dist/skills/fp-pack/* .claude/skills/fp-pack/
If your tool expects a single file, point it to .claude/skills/fp-pack/SKILL.md or link that file to .claude/skills/fp-pack.md.
Copy the Codex skill to your project's $CODEX_HOME/skills/ directory (default: ~/.codex/skills):
# Unix/macOS/Linux
mkdir -p ~/.codex/skills/fp-pack
cp -R node_modules/fp-pack/dist/skills/fp-pack/* ~/.codex/skills/fp-pack/
# Windows (PowerShell)
New-Item -ItemType Directory -Force -Path "$HOME/.codex/skills/fp-pack"
Copy-Item node_modules/fp-pack/dist/skills/fp-pack/* $HOME/.codex/skills/fp-pack -Recurse
Once configured, AI assistants will automatically apply fp-pack coding patterns when helping you write code.
Note: The skills package is located at
node_modules/fp-pack/dist/skills/fp-pack/after installation (includesSKILL.md,examples/,reference/, andconstraints/). You can also viewSKILL.mdin the GitHub repository.
For agents with system prompt support (OpenCode, custom agents), fp-pack provides a reusable behavior module that conditionally enforces fp-pack patterns when fp-pack is detected in the project.
Unlike skills packages which are project-specific, this add-on is attached directly to your agent's system prompt, making it work across all your projects. It automatically activates only when fp-pack is installed.
📖 View AI Agent Role Add-on Documentation
The add-on is located at node_modules/fp-pack/dist/ai-addons/fp-pack-agent-addon.md after installation.
pipe composes functions left-to-right. Use functions-first to create a reusable pipeline, or value-first to execute immediately (value-first often improves type inference because the input anchors generics).
import { pipe, map, filter, take } from 'fp-pack';
// Synchronous data transformation
const processUsers = pipe(
filter((user: User) => user.age >= 18),
map(user => user.name.toUpperCase()),
take(10)
);
const result = processUsers(users);
Data-first invocation (recommended for inference): Pass the input as the first argument to execute immediately.
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)
); // [4, 8]
Constant values with from (optional): Use from to inject constants into pipelines or to start a pipeline with no input.
import { pipe, from, filter, map } from 'fp-pack';
const processData = pipe(
from([1, 2, 3, 4, 5]),
filter((n: number) => n % 2 === 0),
map(n => n * 2)
);
const result = processData(); // [4, 8]
import { pipeAsync } from 'fp-pack';
// Async pipe composition
const fetchUserProfile = pipeAsync(
async (userId: string) => fetch(`/api/users/${userId}`),
async (response) => response.json(),
(data) => data.profile
);
const profile = await fetchUserProfile('user-123');
Need stricter mismatch detection? Use pipeAsyncStrict.
import { pipe, pick, mapValues, assoc } from 'fp-pack';
// Transform and clean data
const prepareUserData = pipe(
pick(['name', 'email', 'age']),
mapValues((val) => typeof val === 'string' ? val.trim() : val),
assoc('timestamp', Date.now())
);
const cleanData = prepareUserData(rawUserInput);
import { pipe } from 'fp-pack';
import { filter, map, take, toArray, range } from 'fp-pack/stream';
// Process only what you need - memory efficient
const processLargeDataset = pipe(
filter((n: number) => n % 2 === 0),
map(n => n * n),
take(100),
toArray
);
// Only processes 100 items, not 1 million
const result = processLargeDataset(range(1, 1000000));
When you write your own helpers that should compose cleanly with pipe/pipeAsync, follow these conventions:
curry(fn) directly.// Fixed signature: curry is enough
function split(separator: string, str: string): string[] {
return str.split(separator);
}
export default curry(split);
// Generic signature: add a type alias for the curried form
type Chunk = {
(size: number): <T>(arr: T[]) => T[][];
<T>(size: number, arr: T[]): T[][];
};
function chunk<T>(size: number, arr: T[]): T[][] {
// ...
return [];
}
const curriedChunk = curry(chunk) as Chunk;
export default curriedChunk;
Functions for composing and transforming other functions.
SideEffect<any>/any, the result becomes any; provide explicit type parameters runPipeResult<SuccessType, ErrorType> to recover a safe union. When the input is narrowed to SideEffect<R> (e.g. after isSideEffect), runPipeResult returns R. Use isSideEffect for precise type narrowing.Functions for conditional logic and flow control.
Functions for working with arrays. All operations are immutable and return new arrays.
Functions for working with objects and records. All operations are immutable.
Functions for comparing and checking values.
Mathematical operations and utilities.
Functions for string manipulation. All operations return new strings.
Functions for asynchronous operations.
Memory-efficient lazy evaluation for large datasets. Works with both sync and async iterables.
Functions for handling nullable values safely.
Functions for debugging and development.
The JavaScript exception problem: In functional pipelines, throwing exceptions breaks composition—control jumps out of the pipe. To avoid this, you need try-catch (which breaks flow) or wrap every function in Either/Result (which requires map/chain everywhere). Both solutions make you think about error plumbing instead of business logic.
The SideEffect solution: Write normal functions that compose naturally. When you need to terminate early (validation failure, missing data, errors), return SideEffect.of(() => ...). pipeSideEffect/pipeAsyncSideEffect pipelines automatically stop—no ceremony, no wrappers, no plumbing. For stricter union typing across branches, use pipeSideEffectStrict / pipeAsyncSideEffectStrict.
import { pipeSideEffect, SideEffect, runPipeResult } from 'fp-pack';
// Optional chaining pattern - return null to gracefully terminate
const findUser = (id: string) => {
const user = database.get(id);
return user ? user : SideEffect.of(() => null);
};
const emailPipeline = pipeSideEffect(
findUser,
(user) => user.email, // Skipped if user not found
(email) => email.toLowerCase()
);
// runPipeResult must be called OUTSIDE the pipeline
const email = runPipeResult(emailPipeline('unknown-id'));
// Returns null without errors - clean optional flow
// Practical: User notification flow
const paymentPipeline = pipeSideEffect(
validateCard,
(card) => card.balance >= 100
? card
: SideEffect.of(() => {
showToast('Insufficient balance');
logEvent('payment_failed', { reason: 'insufficient_funds' });
return null;
}),
chargeCard,
sendReceipt,
(receipt) => ({ success: true, receipt })
);
// runPipeResult must be called OUTSIDE the pipeline
const result = runPipeResult(paymentPipeline(userCard));
// If balance insufficient: shows toast, logs event, returns null
// Otherwise: completes payment and returns success object
Key benefits:
SideEffect.of()pipeSideEffect/pipeAsyncSideEffect automatically short-circuit on SideEffectrunPipeResult / matchSideEffect must be called OUTSIDE the pipeline for proper type safetyType-safe result handling with isSideEffect:
import { pipeSideEffect, pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
const processNumbers = pipeSideEffect(
(nums: number[]) => nums.filter(n => n % 2 === 1),
(odds) => odds.length > 0
? odds
: SideEffect.of(() => 'No odd numbers found'),
(odds) => odds.map(n => n * 2)
);
const oddsDoubled = processNumbers([1, 2, 3, 4, 5]);
// ✅ CORRECT: Use isSideEffect for type checking
if (!isSideEffect(oddsDoubled)) {
// TypeScript knows: oddsDoubled is number[]
const sum: number = oddsDoubled.reduce((a, b) => a + b, 0);
console.log(`Sum: ${sum}`); // sum: number
} else {
// pipeSideEffect widens SideEffect to any, so runPipeResult becomes any here
const error = runPipeResult(oddsDoubled);
console.log(`Error: ${error}`); // error: any
}
// ✅ If you have a precise SideEffect type, runPipeResult returns the effect type
const strictResult = pipeSideEffectStrict(
(n: number) => (n > 0 ? n : SideEffect.of(() => 'LOW' as const))
)(-1);
if (isSideEffect(strictResult)) {
const error = runPipeResult(strictResult);
// error: 'LOW'
}
// ⚠️ If the result type is widened, inference is lost
const widened: number[] | SideEffect<any> = oddsDoubled;
const unsafeResult = runPipeResult(widened); // result: any
// ✅ CORRECT: Provide generics to recover a safe union
const safeResult = runPipeResult<number[], string>(oddsDoubled); // result: number[] | string (union type - safe but not narrowed)
⚠️ CRITICAL: runPipeResult Type Safety
runPipeResult<T, R=any> has a default type parameter R=any. This means:
T | SideEffect<'E'> preserves T | 'E' without extra annotations.T | SideEffect<any> (or any) collapses to any.runPipeResult<SuccessType, ErrorType>(result) restores a safe union when inference is lost.SideEffect<'E'> (e.g. inside if (isSideEffect(...))), runPipeResult returns 'E'.Provide generics when inference is lost; prefer isSideEffect for precise narrowing.
Default choice: Start with pipe / pipeAsync
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.
Pure Pipelines:
pipe - Synchronous, pure transformations (99% of cases) - DX-optimized for global type inferencepipeStrict - Sync pipe with stricter type checking (catches mismatches earlier at connection points)pipeAsync - Async, pure transformations (99% of cases) - DX-optimized for global type inferencepipeAsyncStrict - Async pipe with stricter type checkingSideEffect-Aware Pipelines:
pipeSideEffect - Only when you need SideEffect short-circuiting (sync)pipeSideEffectStrict - Sync SideEffect pipelines with strict effect unionspipeAsyncSideEffect - Only when you need SideEffect short-circuiting (async)pipeAsyncSideEffectStrict - Async SideEffect pipelines with strict effect unionsImportant:
pipe and pipeAsync are for pure functions only—they don't handle SideEffect. If your pipeline can return SideEffect, use pipeSideEffect or pipeAsyncSideEffect instead.pipe/pipeAsync prioritize global type stability (TypeScript infers the final result perfectly without manual annotations). Strict variants (pipeStrict, pipeAsyncStrict) catch type mismatches earlier but may require more type hints. Choose based on your needs: maximum inference power (standard) vs early error detection (strict).// Pure sync pipe - no SideEffect handling
const processNumbers = pipe(
filter((n: number) => n > 0),
map(n => n * 2),
sum
);
// Pure async pipe - no SideEffect handling
const fetchAndProcess = pipeAsync(
async (id: string) => fetchUser(id),
(user) => user.profile, // Sync step is OK
async (profile) => enrichProfile(profile)
);
// SideEffect-aware sync pipe
const validateAndProcess = pipeSideEffect(
(n: number) => n > 0 ? n : SideEffect.of(() => 'Invalid'),
(n) => n * 2
);
// SideEffect-aware async pipe
const fetchAndValidate = pipeAsyncSideEffect(
async (id: string) => fetchUser(id),
(user) => user.verified ? user : SideEffect.of(() => 'Not verified')
);
🔄 Critical Rule: SideEffect Contagion
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.
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.
import { pipe, pipeSideEffect, SideEffect } from 'fp-pack';
const validateUserPipeline = pipeSideEffect(
findUser,
validateAge
);
// Result type: User | SideEffect
// ❌ WRONG - pipe cannot handle SideEffect
const wrongPipeline = pipe(
validateUserPipeline, // Returns User | SideEffect
(user) => user.email // Type error! SideEffect has no 'email' property
);
// ✅ CORRECT - Keep using pipeSideEffect
const correctPipeline = pipeSideEffect(
validateUserPipeline, // User | SideEffect - handled correctly
(user) => user.email, // Automatically skipped if SideEffect
sendEmail
);
// The same rule applies to async pipes
const asyncPipeline = pipeAsyncSideEffect(
fetchUser,
validateUser
);
// Result type: Promise<User | SideEffect>
// You must continue with pipeAsyncSideEffect, not pipeAsync
const extendedAsyncPipeline = pipeAsyncSideEffect(
asyncPipeline,
processUser,
saveToDatabase
);
Use stream/* for:
import { pipe } from 'fp-pack';
import * as Stream from 'fp-pack/stream';
// Memory efficient - processes only 10 items
const first10Evens = pipe(
Stream.filter((n: number) => n % 2 === 0),
Stream.take(10),
Stream.toArray
);
first10Evens(Stream.range(1, 1000000)); // Only processes ~10 items, not 1 million
// Main library (implement/*)
import { pipe, map, filter, pipeAsync } from 'fp-pack';
// Stream functions (lazy iterables)
import { map, filter, toArray, range } from 'fp-pack/stream';
# Install dependencies
pnpm install
# Build library
pnpm build
# Run tests
pnpm test
# Run dev server
pnpm dev
fp-pack draws inspiration from excellent functional programming libraries in the JavaScript ecosystem:
MIT
FAQs
Functional programming utilities library for TypeScript
We found that fp-pack demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.