A modern functional utility library with high performance and strong type safety
npm install fncore
한국어 | English
A modern functional library with high performance and strong type safety
- High Performance: Optimized execution through lazy evaluation
- Type Safety: Written in TypeScript with perfect type inference
- Async Processing: Efficient parallel processing with concurrent and concurrentPool
- Functional Programming: Declarative and readable code with pipe
- Tree Shaking: Minimal bundle size with ESM support
``bash`
npm install fncore
- pipe - Function pipeline compositioncurry
- - Function curryingidentity
- - Identity function
- map, filter, reduce - Basic transformation functionsflatMap
- , flat - Flatteningtake
- , takeWhile, takeUntilInclusive - Conditional extractionchunk
- , slice - Splittingconcat
- , zip - Combiningcompact
- , filterNonNil - Remove null/undefineduniq
- , uniqBy - Remove duplicatesdifference
- , differenceBy - Set differenceintersection
- , intersectionBy - Set intersectionpartition
- - Conditional separationsort
- , sortBy - Sortingreverse
- - Reverse orderhead
- , last - First/last elementpluck
- - Property extractionrange
- - Generate number rangecycle
- - Infinite repetitionscan
- - Cumulative transformationsome
- , every - Condition checkingeach
- - Iteration
- concurrent - Unlimited parallel processingconcurrentPool
- - Limited concurrency controltoAsync
- - Sync → Async conversion
- entries - Object entries conversiongroupBy
- - GroupingindexBy
- - IndexingpickBy
- , omitBy - Conditional select/excludecompactObject
- - Remove falsy valuesprop
- - Property access
- isString, isNumber, isBoolean - Basic type checksisArray
- , isObject - Complex type checksisNil
- , isUndefined, isEmpty - null/undefined/empty checksnegate
- - Condition negationunless
- - Conditional execution
- clamp - Range limiting
- delay - Delayed executionretry
- - Retry logicwithTimeout
- - Timeout setting
- tap - Execute side effects while preserving valuepeek
- - Side effects during iterationnoop
- - No-op function
- throwError - Throw errorthrowIf
- - Conditional error throwing
- toArray - Convert iterable to arraytoAsync
- - Convert sync iterable to async
`typescript
import { pipe, map, filter, take, toArray } from 'fncore';
// Simple pipeline: transform → filter → limit
const result = pipe(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
map((x) => x * 2), // Double each number
filter((x) => x > 10), // Keep only numbers > 10
take(3), // Take first 3 results
toArray
);
// Result: [12, 14, 16]
`
`typescript
import { pipe, toAsync, map, concurrent, retry, toArray } from 'fncore';
// Pure fetch function
const fetchUser = async (id: number) => {
const res = await fetch(/api/users/${id});
if (!res.ok) throw new Error('Failed');
return res.json();
};
// Fetch multiple users in parallel with retry logic
async function fetchUsers(userIds: number[]) {
return pipe(
userIds,
toAsync,
// Apply retry to each fetch operation
map((id) => retry(() => fetchUser(id), { retries: 3, delay: 1000 })),
// Execute 3 requests in parallel
concurrent(3),
toArray
);
}
`
`typescript
import { pipe, groupBy, entries, map, sortBy, filter, toArray } from 'fncore';
interface Order {
userId: string;
amount: number;
status: 'pending' | 'completed' | 'cancelled';
}
// Calculate total amount per user from completed orders
function calculateUserTotals(orders: Order[]) {
return pipe(
orders,
// Filter completed orders only
filter((order) => order.status === 'completed'),
// Group by user ID
groupBy((order) => order.userId),
// Convert to entries for processing
entries,
// Calculate total and count for each user
map(([userId, userOrders]) => ({
userId,
total: userOrders.reduce((sum, order) => sum + order.amount, 0),
count: userOrders.length,
})),
// Sort by total amount (descending)
sortBy((user) => -user.total),
toArray
);
}
`
`typescript
import { pipe, chunk, toAsync, map, concurrent, flat, toArray } from 'fncore';
// Process large dataset in batches to avoid memory issues
async function processBatchData(items: string[]) {
return pipe(
items,
// Split into chunks of 1000 items
chunk(1000),
toAsync,
// Send each batch to API
map(async (batch) => {
const response = await fetch('/api/batch', {
method: 'POST',
body: JSON.stringify(batch),
});
return response.json();
}),
// Process 2 batches in parallel
concurrent(2),
// Flatten results from all batches
flat,
toArray
);
}
`
`typescript
import { pipe, scan, toArray } from 'fncore';
interface CartItem {
name: string;
price: number;
}
// Calculate running total as items are added to cart
const cartItems: CartItem[] = [
{ name: 'Laptop', price: 1000 },
{ name: 'Mouse', price: 50 },
{ name: 'Keyboard', price: 100 },
{ name: 'Monitor', price: 300 },
];
// Using with seed value
const runningTotals = toArray(
scan((total, item) => total + item.price, 0, cartItems)
);
// Result: [0, 1000, 1050, 1150, 1450]
// Without seed - uses first item as initial value
const totals = pipe(
[1, 2, 3, 4],
scan((acc, cur) => acc * cur),
toArray
);
// Result: [1, 2, 6, 24]
// Or use iterator directly
const iter = scan((total, item) => total + item.price, 0, cartItems);
iter.next(); // {value: 0, done: false}
iter.next(); // {value: 1000, done: false}
iter.next(); // {value: 1050, done: false}
`
`typescript
import { pipe, scan, toAsync, toArray } from 'fncore';
interface Notification {
type: 'message' | 'like' | 'comment';
count: number;
}
// Real-time notification stream
async function* notificationStream() {
// Simulating real-time notifications
yield { type: 'message' as const, count: 1 };
yield { type: 'like' as const, count: 3 };
yield { type: 'comment' as const, count: 2 };
yield { type: 'message' as const, count: 1 };
}
// Track cumulative notification count (with seed)
const totalNotifications = scan(
(total, notification) => total + notification.count,
0,
notificationStream()
);
for await (const total of totalNotifications) {
updateBadge(total); // Update UI badge with total count
// Yields: 0, 1, 4, 6, 7
}
// Or use pipe without seed (first item becomes initial value)
const counts = [1, 3, 2, 1];
const totals = await pipe(
counts,
toAsync,
scan((total, count) => total + count),
toArray
);
// Result: [1, 4, 6, 7]
`
`typescript
import { pipe, retry, toAsync, map, filter, toArray } from 'fncore';
// Pure fetch functions - no retry logic inside
const fetchOrder = async (id: number) => {
const res = await fetch(/api/orders/${id});
if (!res.ok) throw new Error('Failed');
return res.json();
};
const fetchOrderDetails = async (order: Order) => {
const res = await fetch(/api/orders/${order.id}/details);
if (!res.ok) throw new Error('Failed');
const details = await res.json();
return { ...order, details };
};
// Multi-step pipeline: fetch orders → filter completed → fetch details
async function processOrders(orderIds: number[]) {
return pipe(
orderIds,
toAsync,
// Step 1: Fetch all orders with retry
map((id) => retry(() => fetchOrder(id), { retries: 3, delay: 1000 })),
// Step 2: Filter only completed orders
filter((order) => order.status === 'completed'),
// Step 3: Fetch details for each completed order with retry
map((order) =>
retry(() => fetchOrderDetails(order), { retries: 3, delay: 1000 })
),
toArray
);
}
`
`typescript
import { pipe, toAsync, map, concurrentPool, retry, toArray } from 'fncore';
// API with rate limit (e.g., max 5 requests per second)
const fetchUserProfile = async (id: number) => {
const res = await fetch(/api/users/${id}/profile);
if (!res.ok) throw new Error('Failed');
return res.json();
};
// Process 1000 users while respecting rate limits
async function fetchAllUserProfiles(userIds: number[]) {
return pipe(
userIds,
toAsync,
// Apply retry to each request
map((id) => retry(() => fetchUserProfile(id), { retries: 3, delay: 1000 })),
// Limit to 5 concurrent requests to respect rate limits
concurrentPool(5),
toArray
);
}
// Example: Processing 1000 users
// - Without concurrentPool: All 1000 requests at once → Rate limit error
// - With concurrent(5): 5 at a time, but no control over timing
// - With concurrentPool(5): Exactly 5 concurrent requests, respects API limits
`
`typescript
import {
pipe,
map,
filter,
throwIf,
unless,
negate,
isEmpty,
toArray,
} from 'fncore';
interface UserInput {
email: string;
age: number;
name: string;
}
// Validate and process user inputs with clear error handling
function processUserInputs(inputs: UserInput[]) {
return pipe(
inputs,
// Validate: throw error if email is empty
map((user) =>
throwIf(
(u) => isEmpty(u.email),
() => new Error('Email is required')
)(user)
),
// Validate: throw error if age is invalid
map((user) =>
throwIf(
(u) => u.age < 18 || u.age > 100,
() => new Error('Invalid age')
)(user)
),
// Filter: keep only valid names (skip empty names instead of throwing)
filter((user) => !isEmpty(user.name)),
// Transform: normalize email
map((user) => ({ ...user, email: user.email.toLowerCase().trim() })),
toArray
);
}
// Conditional execution with unless
const processAdult = unless(
(user: UserInput) => user.age < 18,
(user) => {
console.log(Processing adult user: ${user.name});
return user;
}
);
// Using negate for cleaner conditions
const isNotEmpty = negate(isEmpty);
const validEmails = pipe(
['test@example.com', '', 'admin@test.com', null, 'user@domain.com'],
filter(isNotEmpty),
toArray
);
// Result: ['test@example.com', 'admin@test.com', 'user@domain.com']
`
`typescript
import {
pipe,
entries,
map,
filter,
pickBy,
omitBy,
compactObject,
toArray,
} from 'fncore';
interface Product {
id: string;
name: string;
price: number;
stock: number;
discount?: number;
tags?: string[];
}
// Clean and transform product data
function cleanProductData(products: Product[]) {
return pipe(
products,
// Remove products with invalid data
map((product) => compactObject(product)),
// Keep only products with positive stock
map(
pickBy(
([key, value]) =>
key !== 'stock' || (typeof value === 'number' && value > 0)
)
),
// Remove internal fields
map(omitBy(([key]) => key.startsWith('_'))),
toArray
);
}
// Dynamic filtering based on object properties
const activeProducts = pipe(
products,
map((p) => entries(p)),
map((entries) =>
entries.filter(([key, value]) => value !== null && value !== undefined)
),
map((entries) => Object.fromEntries(entries)),
toArray
);
`
`typescript
import { pipe, map, filter, curry, toArray } from 'fncore';
// Create reusable curried functions
const add = curry((a: number, b: number) => a + b);
const multiply = curry((a: number, b: number) => a * b);
const greaterThan = curry(
(threshold: number, value: number) => value > threshold
);
// Use in pipelines
const add10 = add(10);
const multiplyBy2 = multiply(2);
const greaterThan50 = greaterThan(50);
const result = pipe(
[1, 2, 3, 4, 5],
map(add10), // [11, 12, 13, 14, 15]
map(multiplyBy2), // [22, 24, 26, 28, 30]
filter(greaterThan50), // []
toArray
);
// Practical example: Price calculations
const applyTax = curry((rate: number, price: number) => price * (1 + rate));
const applyDiscount = curry(
(rate: number, price: number) => price * (1 - rate)
);
const calculateFinalPrice = pipe(
[100, 200, 300],
map(applyDiscount(0.1)), // 10% discount
map(applyTax(0.2)), // 20% tax
toArray
);
// Result: [108, 216, 324]
`
`typescript
import { pipe, map, filterNonNil, toArray } from 'fncore';
interface User {
id: number;
name: string;
email: string | null;
profileImage: string | null;
lastLoginAt: Date | null;
}
// Database query returns nullable fields even with WHERE clause
async function getUsersWithEmail() {
// DB returns User[] but TypeScript still sees email as string | null
const users: User[] = await db.query(
'SELECT * FROM users WHERE email IS NOT NULL'
);
return pipe(
users,
// filterNonNil('email') filters AND narrows the type
filterNonNil('email'),
// Now TypeScript knows email is string (not string | null)
map((user) => user.email.toLowerCase()), // No type error
toArray
);
}
// Practical example: Processing orders with required fields
interface Order {
orderId: number;
userId: number | null;
productId: number | null;
shippingAddress: string | null;
paymentMethod: string | null;
}
async function getValidOrders(orders: Order[]) {
return pipe(
orders,
// Filter orders with userId (type narrows to number)
filterNonNil('userId'),
// Filter orders with productId (type narrows to number)
filterNonNil('productId'),
// Filter orders with shipping address (type narrows to string)
filterNonNil('shippingAddress'),
// Now TypeScript knows all fields are non-null
map((order) => ({
orderId: order.orderId,
userId: order.userId, // Type: number (not number | null)
productId: order.productId, // Type: number (not number | null)
shippingAddress: order.shippingAddress, // Type: string (not string | null)
})),
toArray
);
}
// Real-world example: User profiles with optional data
interface UserProfile {
userId: number;
displayName: string | null;
bio: string | null;
avatarUrl: string | null;
verifiedAt: Date | null;
}
async function getVerifiedProfiles(profiles: UserProfile[]) {
return pipe(
profiles,
// Only verified users
filterNonNil('verifiedAt'),
// Only users with display name
filterNonNil('displayName'),
// Only users with avatar
filterNonNil('avatarUrl'),
// All fields are now guaranteed non-null
map((profile) => ({
userId: profile.userId,
displayName: profile.displayName.trim(), // ✅ Safe string operation
avatarUrl: profile.avatarUrl.replace('http://', 'https://'), // ✅ Safe
verifiedAt: profile.verifiedAt.toISOString(), // ✅ Safe Date operation
})),
toArray
);
}
``
MIT © Wisely Company
This library is inspired by:
- es-toolkit - MIT License
Copyright (c) 2024 Viva Republica, Inc
- fxts - Apache License 2.0
Copyright FxTS contributors