fp
is a lightweight TypeScript library designed to simplify functional programming by providing essential types like Option
and Result
. It helps developers handle errors, manage optional values, and write expressive, composable code.
npm i @carbonteq/fp
pnpm i @carbonteq/fp
yarn add @carbonteq/fp
bun add @carbonteq/fp
In JavaScript and TypeScript, dealing with null
, undefined
, and errors can lead to verbose, error-prone code. fp
introduces functional paradigms that make handling these cases cleaner and more reliable. By leveraging fp
, you can reduce boilerplate, improve readability, and create more maintainable applications.
To demonstrate the utility of fp
, let us consider a use case where we need to retrieve a user's email and address.
function getUserByEmail(user: { email?: string }): string | null {
return user.email ? user.email : null;
}
function getUserAdress(user: { email?: string }): string | null {
return user.email ? "Some Address" : null;
}
const email = getUserByEmail({ email: "[email protected]" });
if (email) {
const address = getUserAdress(email);
if (address) {
console.log(`User ${email} has address: ${address}`);
} else {
console.error("Address not found");
}
} else {
console.error("Email not found");
}
// Output: User test@test.com has address: Some Address
Now imagine if we had more complex use cases that involved more than two optional values. We would have to nest if statements and handle errors manually. This is where fp
comes in.
import { Option, matchOpt } from "@carbonteq/fp";
function getUserByEmail(user: { email?: string }): Option<string> {
return user.email ? Option.Some(user.email) : Option.None;
}
function getUserAddress(user: { email?: string }): Option<string> {
return user.email ? Option.Some("Some Address") : Option.None;
}
const res = getUserByEmail({ email: "[email protected]" }).flatZip((email) =>
getUserAddress({ email }),
);
matchOpt(res, {
Some: (res) => {
console.log(`User ${res[0]} has address: ${res[1]}`);
},
None: () => {
console.error("User or Address not found");
},
});
// Output: User test@test.com has address: Some Address
Result
represents a value that can be either a success (Ok
) or a failure (Err
). It simplifies error handling by making success and failure explicit.
import { Result } from "@carbonteq/fp";
const res1: Result<number, string> = Result.Ok(5); // Contains the value 5
const res2: Result<number, string> = Result.Err("Some Error"); // Contains the error "Some Error"
The Option
type represents a value that might or might not be present. It eliminates the need for manual checks for null
or undefined
.
import { Option } from "@carbonteq/fp";
const opt1: Option<number> = Option.Some(5); // Contains the value 5
const opt2: Option<number> = Option.None; // Contains no value (None)
Transforms the Some
or Ok
value inside Option
or Result
.
Let's say we want to apply a 10% bonus to the account balance of a user.
import { Option } from "@carbonteq/fp";
async function fetchUserBalanceFromDatabase(
userId: string,
): Promise<Option<number>> {
// Simulate fetching balance from a database
await Promise.resolve(userId);
return Option.Some(100);
}
async function applyBonus() {
const userId = "user123"; // Example user ID
const balanceOption = await Option.Some(userId)
.flatMap(fetchUserBalanceFromDatabase)
.map((balance) => balance * 1.1)
.toPromise(); // Apply a 10% bonus if balance exists
return balanceOption;
}
console.log(await applyBonus()); // Output: Some(110)
flatMap
is used to chain operations where each step returns an Option
or Result
. It avoids nested structures like Option<Option<T>>
or Result<Result<T, E>, E>
by "flattening" them into a single level.
Let's say we want to validate user input and then save it to a database:
import { Result } from "@carbonteq/fp";
// Function to validate user input
const validateUserData = (
name: string,
age: number,
): Result<{ name: string; age: number }, Error> => {
if (name.trim() === "") {
return Result.Err(new Error("Name cannot be empty"));
}
if (age < 0 || age > 120) {
return Result.Err(new Error("Age must be between 0 and 120"));
}
return Result.Ok({ name, age });
};
// Simulated asynchronous task to save user data to a "database"
const saveUserData = async (user: {
name: string;
age: number;
}): Promise<Result<string, Error>> => {
// Simulate saving to database
await Promise.resolve(user);
return Result.Ok(`User ${user.name} saved successfully!`);
};
// Chaining validation and save using `flatMap`
const processUser = async (
name: string,
age: number,
): Promise<Result<string, Error>> => {
const validationResult = await validateUserData(name, age)
.flatMap(saveUserData)
.toPromise();
return validationResult;
};
console.log(await processUser("Alice", 30)); // Output: Result.Ok("User Alice saved successfully!")
/* validationResult is of type Result<string, Error> instead of Result<Result<string, Error>, Error> (which is what would have happened if we used map instead of flatMap) */
Creates a tuple [T, U]
where the second value U
is derived from the first value T
using a function f
.
For example, suppose we want to pair the product's original price T
and discounted price U
.
import { Result } from "@carbonteq/fp";
async function fetchProductPrice(
productId: string,
): Promise<Result<number, Error>> {
// Simulate fetching price from a database
await Promise.resolve(productId);
return Result.Ok(100);
}
async function applyDiscount(
productId: string,
): Promise<Result<[number, number], Error>> {
const originalPrice = await Result.Ok(productId)
.flatMap(fetchProductPrice)
.zip((price) => price * 0.9)
.toPromise();
return originalPrice; //originalPrice is of type Result<[number, number], Error>
}
console.log(await applyDiscount("123")); // Output: Result.Ok([100, 90])
Here derivedPair
is a Result<[number, number], Error>
. Note that T
= 100
and U
= 90
.
Combines the current Result<T, E>
with another Result<U, E2>
, or the current Option<T>
with another Option<U>
. Unlike zip, which pairs a value with a derived one, flatZip works with two independent Result/Option values and combines their contents into a tuple [T, U]
. It ensures the Result/Option values are Ok/Some to proceed; otherwise, it propagates the first encountered Err/None.
Lets say we want to combine the product price and product stock into a tuple [number, number]
.
import { Option } from "@carbonteq/fp";
// Simulated function to fetch product price
async function fetchProductPrice(productId: string): Promise<Option<number>> {
// Simulate fetching price from a database
await Promise.resolve(productId);
return Option.Some(100);
}
// Simulated function to fetch product stock
async function fetchProductStock(productId: string): Promise<Option<number>> {
// Simulate fetching stock from a database
await Promise.resolve(productId);
return Option.Some(50);
}
// Function to combine price and stock using flatZip
async function fetchProductDetails(
productId: string,
): Promise<Option<[number, number]>> {
const productDetails = await Option.Some(productId)
.flatMap(fetchProductPrice)
.flatZip(fetchProductStock)
.toPromise();
return productDetails; // Option<[number, number]>
}
console.log(await fetchProductDetails("123")); // Output: Option.Some([100, 50])
Method | map |
flatMap |
zip |
flatZip |
---|---|---|---|---|
Purpose | Transforms the value inside an Ok or Some . |
Chains dependent computations where each computation returns a Result or Option . |
Combines the current value with another derived value into a tuple [T, U] . |
Combines two independent Result or Option values into a tuple [T, U] . |
Input | A function (val: T) => U . |
A function (val: T) => Result<U, E2> or (val: T) => Option<U> to transform the current value into another Result or Option . |
A function (val: T) => U to derive a new value U from the current value T . |
A function (val: T) => Result<U, E2> or (val: T) => Option<U> that returns another Result or Option . |
Output | Result<U, E> , Option<U> |
Result<U, E> , Option<U> |
Result<[T, U], E> , Option<[T, U]> |
Result<[T, U], E> , Option<[T, U]> . |
Error Propagation | Propagates Err /None if the Result /Option is Err /None . |
Propagates the first Err /None encountered in the chain. |
Propagates Err /None if the current Result /Option or derived value is Err /None . |
Propagates the first Err /None encountered between the two Result /Option values. |
Use Case | When you want to transform a value inside Ok /Some . |
When the next computation depends on the current value and returns a Result /Option . |
When you want to pair the current value with a derived one. | When you want to combine two independent Result /Option values into a tuple. |
Transforms the Err
value inside Result
.
import { Result } from "@carbonteq/fp";
// Simulated function to divide two numbers
function divideNumbers(a: number, b: number): Result<number, Error> {
if (b === 0) {
return Result.Err(new Error("Division by zero"));
}
return Result.Ok(a / b);
}
// A safe divide function that returns a Result with a string error message if the division by zero occurs, instead of throwing an exception
function safeDivide(a: number, b: number): Result<number, string> {
const res = divideNumbers(a, b).mapErr(
(err) => `Operation failed: ${err.message}`,
);
return res;
}
// Example usage
console.log(safeDivide(10, 2)); // Result.Ok(5)
console.log(safeDivide(10, 0)); // Result.Err("Operation failed: Division by zero")
Checks if the Option
or Result
is Some
or Ok
, and if so, applies the function to the value inside. If the Option
or Result
is None
or Err
, it returns the default value.
import { Option } from "@carbonteq/fp";
// Simulated function to find a user by id
async function findUserById(id: number): Promise<Option<number>> {
// Simulate a database call that either returns a user or null
await Promise.resolve(id);
if (id === 0) {
return Option.None;
}
return Option.Some(id);
}
// A safe function that returns a string error message if the user is not found, instead of throwing an exception
async function safeFindUserById(id: number): Promise<Option<string>> {
const res = (await findUserById(id)).mapOr(
`User not found`,
(res) => `User: ${res}`,
);
return res;
}
// Example usage
console.log(await safeFindUserById(10)); // Some(User: 10)
console.log(await safeFindUserById(0)); // Some(User not found)
all
is used to combine an array of Results. If any errors exist they are accumulated, else the values are accumulated.
import { Result, matchRes } from "@carbonteq/fp";
type User = {
userId: string;
userName: string;
createdAt: Date | string;
};
type Post = {
postId: string;
likes: number;
replies: number;
createdAt: Date | string;
author: User["userId"];
};
type Like = {
likeId: string;
postId: Post["postId"];
createdAt: Date | string;
likedBy: User["userId"];
};
type Reply = {
replyId: string;
postId: Post["postId"];
createdAt: Date | string;
author: User["userId"];
};
type Hash = string;
async function fetchUser(userId: string): Promise<Result<User, unknown>> {
return Result.Ok({
userId,
userName: "Functional Programmer",
createdAt: "2025-01-01",
});
}
async function fetchPosts(userId: string): Promise<Result<Post[], string>> {
if (userId === "TRIAL_USER") {
return Result.Err("User has no posts!");
}
return Result.Ok([
{
postId: "1",
likes: 12,
replies: 3,
createdAt: "2025-01-01",
author: userId,
},
]);
}
async function fetchLikes(userId: string): Promise<Result<Like[], unknown>> {
return Result.Ok([
{ likeId: "3", postId: "2", createdAt: "2025-01-01", likedBy: userId },
]);
}
async function fetchReplies(userId: string): Promise<Result<Reply[], string>> {
if (userId === "TRIAL_USER") {
return Result.Err("User has no replies!");
}
return Result.Ok([
{
replyId: "1",
postId: "2",
data: "Nice post!",
createdAt: "2025-01-01",
author: userId,
},
]);
}
function generateHash(userId: string): Result<Hash, Error> {
return Result.Ok(`${userId}_HASH_VALUE`);
}
async function getUserData(userId: string) {
const userIdRes = Result.Ok(userId);
const user = userIdRes.flatMap(fetchUser);
const posts = userIdRes.flatMap(fetchPosts);
const likes = userIdRes.flatMap(fetchLikes);
const replies = userIdRes.flatMap(fetchReplies);
const hash = userIdRes.flatMap(generateHash);
const userData = await Result.all(
user, // Result<Promise<T>, E>
posts, // Result<Promise<T>, E>
likes, // Result<Promise<T>, E>
replies, // Result<Promise<T>, E>
hash, // Result<T, E>
).toPromise();
matchRes(userData, {
Ok(v) {
console.log(v);
},
Err(e) {
console.log(e);
},
});
}
await getUserData("USER_ID");
// [
// {
// userId: 'USER_ID',
// userName: 'Functional Programmer',
// createdAt: '2025-01-01'
// },
// [
// {
// postId: '1',
// likes: 12,
// replies: 3,
// createdAt: '2025-01-01',
// userId: 'USER_ID'
// }
// ],
// [ { postId: '2', createdAt: '2025-01-01', userId: 'USER_ID' } ],
// [
// {
// postId: '2',
// data: 'Nice post!',
// createdAt: '2025-01-01',
// userId: 'USER_ID'
// }
// ],
// 'USER_ID_HASH_VALUE'
// ]
await getUserData("TRIAL_USER");
// [ 'User has no posts!', 'User has no replies!' ]
Built on top of all
, validate
is used to execute an array of validator functions in parallel. If all validations pass, the original value is retained. If any validations fail, the errors are accumulated. Both synchronous and asynchronous computations are handled.
import { Result } from "@carbonteq/fp";
function hasMinimumLength(password: string): Result<boolean, Error> {
return password.length < 8
? Result.Err(new Error("Password must be at least 8 characters"))
: Result.Ok(true);
}
function hasSpecialCharacters(password: string): Result<boolean, Error> {
const specialCharsRegex = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/;
return !specialCharsRegex.test(password)
? Result.Err(
new Error("Password must contain at least one special character"),
)
: Result.Ok(true);
}
// Asynchronous validation function - checks if password is different from previous
async function isNotSameAsPrevious(
password: string,
): Promise<Result<boolean, Error>> {
// Simulate checking against a database of user's previous passwords
return new Promise<Result<boolean, Error>>((resolve) => {
setTimeout(() => {
// For demo purposes, we'll consider "password123!" as the previous password
if (password === "password123!") {
resolve(
Result.Err(
new Error("New password cannot be the same as previous password"),
),
);
} else {
resolve(Result.Ok(true));
}
}, 200); // simulate network delay
});
}
const validatedOk = Result.Ok("password321!").validate([
hasMinimumLength,
hasSpecialCharacters,
]);
console.log(validatedOk.unwrap()); // password321!
const validatedErr = await Result.Ok("pword")
.validate([hasMinimumLength, hasSpecialCharacters])
.toPromise();
console.log(validatedErr.unwrapErr()); // [Error: Password must be at least 8 characters, Error: Password must contain at least one special character]
const validatedErrs = await Result.Ok("password123!")
.validate([hasMinimumLength, hasSpecialCharacters, isNotSameAsPrevious])
.toPromise();
console.log(validatedErrs.unwrapErr()); // [Error: New password cannot be the same as previous password]
These functions are used to extract the value from a Result
. Note that only unwrap
and safeUnwrap
are available for Option
.
import { Result } from "@carbonteq/fp";
function divideNumbers(a: number, b: number): Result<number, Error> {
if (b === 0) {
return Result.Err(new Error("Division by zero"));
}
return Result.Ok(a / b);
}
// Example usage of each function
let result = divideNumbers(10, 2);
console.log(result.unwrap()); // unwrap: 5 (Extracts the value from Result.Ok, throwing an error if it's Err.)
result = divideNumbers(10, 0);
console.log(result.safeUnwrap()); // safeUnwrap: null (Safely unwraps the value, returning null if it's an Err instead of throwing an error.)
const errorResult = Result.Err(new Error("Something went wrong"));
console.log(errorResult.unwrapErr()); // unwrapErr: Error: Something went wrong (Extracts the error from Result.Err, throwing an error if it's Ok)
Let's build a pipeline for processing an e-commerce order. This example demonstrates handling user input validation, inventory checks, and order processing.
import { Result, matchRes } from "@carbonteq/fp";
interface Order {
productId: string;
quantity: number;
userId: string;
}
interface ProcessedOrder {
orderId: string;
total: number;
status: "confirmed" | "failed";
order: Order;
}
// Validate order input
function guardOrder(order: Order): Result<Order, string> {
if (!order.productId) return Result.Err("Product ID is required");
if (order.quantity <= 0) return Result.Err("Quantity must be positive");
if (!order.userId) return Result.Err("User ID is required");
return Result.Ok(order);
}
// Check product availability
function guardInventoryCheck(order: Order): Result<Order, string> {
const availableStock = 100; // Simulated stock
return order.quantity <= availableStock
? Result.Ok(order)
: Result.Err(`Insufficient stock. Available: ${availableStock}`);
}
// Calculate order total
function calculateTotal(order: Order): Result<number, string> {
const price = 29.99; // Simulated price
return Result.Ok(order.quantity * price);
}
// Process the order
function processOrder(order: Order): Result<ProcessedOrder, string> {
return Result.Ok(order)
.validate([guardOrder, guardInventoryCheck])
.flatZip(calculateTotal)
.map(([order, total]) => ({
orderId: `ORD-${Date.now()}`,
total: total,
status: "confirmed" as const,
order: order,
}))
.mapErr((error) => `Order processing failed: ${error}`);
}
// Usage
const order: Order = {
productId: "PROD-123",
quantity: 2,
userId: "USER-456",
};
const result = processOrder(order);
matchRes(result, {
Ok: (processedOrder) => {
console.log(`Order confirmed! Order ID: ${processedOrder.orderId}`);
console.log(`Total: $${processedOrder.total.toFixed(2)}`);
console.log(`Order: ${JSON.stringify(processedOrder.order)}`);
},
Err: (error) => {
console.error(`Error: ${error}`);
},
});
// Output:
// Order confirmed! Order ID: ORD-1716888600000
// Total: $59.98
// Order: {"productId":"PROD-123","quantity":2,"userId":"USER-456"}
Let's build an asynchronous pipeline for a user registration system that includes credentials verification and profile setup.
import { matchRes } from "@/match.js";
import { Result } from "@/result.js";
interface UserInput {
email: string;
password: string;
name: string;
}
interface UserProfile {
userId: string;
email: string;
name: string;
verificationStatus: "pending" | "verified";
}
// Validate email format (synchronous)
function guardEmail(email: string): Result<string, string> {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email)
? Result.Ok(email)
: Result.Err("Invalid email format");
}
// Check if email is already registered
async function guardEmailAvailability(
email: string,
): Promise<Result<string, string>> {
// Simulate database check
await Promise.resolve(email);
const registeredEmails = ["[email protected]"];
return !registeredEmails.includes(email)
? Result.Ok(email)
: Result.Err("Email already registered");
}
// Validate password strength (synchronous)
function guardPassword(password: string): Result<string, string> {
return password.length >= 8
? Result.Ok(password)
: Result.Err("Password must be at least 8 characters");
}
// Create user profile
async function createUserProfile(
input: UserInput,
): Promise<Result<UserProfile, string>> {
const userProfile: UserProfile = {
userId: `USER-${Date.now()}`,
email: input.email,
name: input.name,
verificationStatus: "pending",
};
return Result.Ok(userProfile);
}
// Send verification email
async function sendVerificationEmail(
profile: UserProfile,
): Promise<Result<UserProfile, string>> {
// Simulate email sending and verification
await Promise.resolve(profile);
console.log(`Verification email sent to ${profile.email}`);
return Result.Ok(profile);
}
// Main registration pipeline
async function registerUser(
input: UserInput,
): Promise<Result<UserProfile, string | string[]>> {
const res = await Result.Ok(input)
.validate([
// handles both sync and async functions
({ email }) => guardEmail(email),
({ email }) => guardEmailAvailability(email),
({ password }) => guardPassword(password),
])
.flatMap(createUserProfile)
.flatMap(sendVerificationEmail)
.toPromise();
return res;
}
// Usage
async function main() {
const userInput: UserInput = {
email: "[email protected]",
password: "securepass123",
name: "John Doe",
};
const result = await registerUser(userInput);
matchRes(result, {
Ok: (profile) => {
console.log(`Email: ${profile.email}`);
console.log("Registration successful!");
console.log(`User ID: ${profile.userId}`);
console.log(`Verification Status: ${profile.verificationStatus}`);
},
Err: (error) => {
console.error(`Registration failed: ${error}`);
},
});
}
main();
// Output:
// Verification email sent to newuser@example.com
// Email: newuser@example.com
// Registration successful!
// User ID: USER-1737921964102
// Verification Status: pending
Contributions are welcome! Feel free to open issues or submit pull requests.