Hey there, fellow coder! đź‘‹ Tired of wrestling with unpredictable outcomes in your TypeScript projects? Say hello to Fates, your new best friend in the battle against uncertainty!
- Introduction
- Key Features
- Installation
- Quick Start
- Core Concepts
- Advanced Usage
- API Reference
- Best Practices and Patterns
- Performance Considerations
- Migrating from Try-Catch
- Comparison with Other Libraries
- Contributing
- License
Fates is a powerful TypeScript library that revolutionizes error handling and data flow management. By introducing a Result
type inspired by functional programming paradigms, Fates enables developers to write more predictable, maintainable, and expressive code.
- 🛡️ Type-Safe Error Handling: Leverage TypeScript's type system for robust error management.
- đź”— Elegant Function Composition: Chain operations seamlessly, even with potential failures.
- 🚀 First-Class Async Support: Handle asynchronous operations with grace and predictability.
- 🧰 Comprehensive Utility Set: A rich collection of tools for manipulating and transforming Results.
- 🏗️ Flexible Validation Framework: Construct complex, reusable validation pipelines with ease.
- 🔄 Built-in Retry Mechanism: Handle transient failures in distributed systems effortlessly.
- 🧩 Powerful Combinators: Combine and manipulate multiple Results with intuitive operators.
Choose your preferred package manager:
npm install fates
# or
yarn add fates
# or
pnpm add fates
Let's dive into a simple example to see Fates in action:
import { ok, err, Result } from 'fates';
function divideAndFormatCurrency(a: number, b: number): Result<string, string> {
if (b === 0) return err("Division by zero");
const result = a / b;
return ok(`$${result.toFixed(2)}`);
}
const result = divideAndFormatCurrency(100, 3);
result.match({
ok: (value) => console.log(`Formatted result: ${value}`),
err: (error) => console.log(`Error occurred: ${error}`),
});
// Output: Formatted result: $33.33
The foundation of Fates is the Result
type:
type Result<T, E = Error> = Ok<T> | Err<E>;
It encapsulates either a success value (Ok<T>
) or a failure value (Err<E>
).
Fates also provides an Option
type for representing values that may or may not exist:
type Option<T> = Ok<T> | Err<null>;
The Option
type is useful for handling nullable values without resorting to null checks:
import { fromNullable, Option } from 'fates';
function findUser(id: number): Option<User> {
const user = database.getUser(id);
return fromNullable(user);
}
const userOption = findUser(1);
userOption.match({
ok: (user) => console.log(`Found user: ${user.name}`),
err: () => console.log("User not found"),
});
Create success and failure results:
import { ok, err } from 'fates';
const success = ok(42);
const failure = err("Something went wrong");
console.log(success.isOk()); // true
console.log(failure.isErr()); // true
Both Ok
and Err
provide powerful methods for working with results:
Pattern match on the result:
result.match({
ok: (value) => console.log(`Success: ${value}`),
err: (error) => console.log(`Error: ${error}`),
});
Transform success values:
const doubled = ok(21).map(x => x * 2); // Ok(42)
const maybeSquared = doubled.flatMap(x => x > 50 ? err("Too large") : ok(x * x)); // Err("Too large")
Transform error values:
const newError = err("error").mapErr(e => new Error(e)); // Err(Error("error"))
Extract values (with caution):
const value = ok(42).unwrap(); // 42
const fallback = err("error").unwrapOr(0); // 0
Extract raw values regardless of error
const value ok(41).safeUnwrap(); // 41
const fallback = err("error").safeUnwrap(); // "error"
const badfallback = err("some error").unwrap(); // Throws "some error"
Fates seamlessly integrates with asynchronous code:
import { fromPromise, AsyncResult } from 'fates';
async function fetchUserData(id: number): AsyncResult<User, Error> {
return fromPromise(fetch(`https://api.example.com/users/${id}`).then(res => res.json()));
}
const result = await fetchUserData(1);
result.match({
ok: (user) => console.log(`Welcome, ${user.name}!`),
err: (error) => console.log(`Fetch failed: ${error.message}`),
});
Elegantly chain operations that might fail:
import { chain } from 'fates';
const getUser = (id: number): Result<User, Error> => { /* ... */ };
const getUserPosts = (user: User): Result<Post[], Error> => { /* ... */ };
const formatPosts = (posts: Post[]): Result<string, Error> => { /* ... */ };
const formattedUserPosts = getUser(1)
.flatMap(chain(getUserPosts))
.flatMap(chain(formatPosts));
formattedUserPosts.match({
ok: (formatted) => console.log(formatted),
err: (error) => console.error(`Error: ${error.message}`),
});
Gracefully handle and transform errors:
import { recover, mapError } from 'fates';
const result = getUserData(1)
.mapErr(error => `Failed to fetch user: ${error.message}`)
.recover(error => ({ id: 0, name: 'Guest' }));
console.log(result.unwrap()); // Either user data or the guest user
Build complex, reusable validation logic:
import { validate, validateAll } from 'fates';
const validateAge = (age: number) => validate(age, a => a >= 18, "Must be an adult");
const validateEmail = (email: string) => validate(email, e => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e), "Invalid email");
const validateUser = (user: { age: number, email: string }) =>
validateAll(user, [
u => validateAge(u.age),
u => validateEmail(u.email)
]);
const result = validateUser({ age: 25, email: "[email protected]" });
result.match({
ok: (user) => console.log(`Valid user: ${JSON.stringify(user)}`),
err: (error) => console.error(`Validation failed: ${error}`),
});
Safely parse and convert data:
import { parseNumber, parseDate, parseJSON } from 'fates';
const num = parseNumber("42").unwrapOr(0);
const date = parseDate("2023-04-01").unwrapOr(new Date());
const config = parseJSON<Config>(jsonString).unwrapOr(defaultConfig);
Work with multiple results simultaneously:
import { all, any, sequenceObject } from 'fates';
const results = all([fetchUser(1), fetchPosts(1), fetchComments(1)]);
results.match({
ok: ([user, posts, comments]) => console.log(`Fetched data for ${user.name}`),
err: (error) => console.error(`One or more fetches failed: ${error}`),
});
const firstSuccess = any([unreliableServiceA(), unreliableServiceB(), unreliableServiceC()]);
firstSuccess.match({
ok: (result) => console.log(`Got a successful result: ${result}`),
err: (errors) => console.error(`All services failed: ${errors.join(', ')}`),
});
const userResult = sequenceObject({
name: validateName(formData.name),
email: validateEmail(formData.email),
age: validateAge(formData.age)
});
Create robust data processing flows:
import { pipeline } from 'fates';
const processOrder = pipeline(
validateOrder,
calculateTotalWithTax,
applyDiscount,
saveToDatabase,
sendConfirmationEmail
);
const result = await processOrder(orderData);
result.match({
ok: (confirmationId) => console.log(`Order processed, confirmation: ${confirmationId}`),
err: (error) => console.error(`Order processing failed: ${error}`),
});
Handle transient failures in distributed systems:
import { retry } from 'fates';
const fetchWithRetry = retry(
() => fetchFromUnreliableService(),
3, // max attempts
1000 // delay between attempts (ms)
);
const result = await fetchWithRetry();
result.match({
ok: (data) => console.log(`Fetched successfully: ${data}`),
err: (error) => console.error(`All attempts failed: ${error}`),
});
For a complete API reference, please visit our API Documentation.
- Prefer
flatMap
overmap
when chaining operations that returnResult
. - Use
tap
for side effects without changing theResult
value. - Leverage
validateAll
for complex object validations. - Use
AsyncResult
consistently for asynchronous operations. - Prefer
recover
overunwrapOr
for more flexible error handling.
Fates is designed with performance in mind, but here are some tips to optimize your use:
- Avoid unnecessary
map
andflatMap
chains when a single operation would suffice. - Use
AsyncResult
directly instead of wrappingPromise<Result<T, E>>
. - Leverage
all
andany
for parallel operations instead of sequentialflatMap
chains.
Fates offers a more expressive and type-safe alternative to traditional try-catch blocks. Here's a quick comparison:
// Traditional try-catch
try {
const result = riskyOperation();
console.log(result);
} catch (error) {
console.error(error);
}
// With Fates
riskyOperation()
.match({
ok: (result) => console.log(result),
err: (error) => console.error(error),
});
Fates draws inspiration from functional programming concepts and libraries like Rust's Result
type. However, it's tailored specifically for TypeScript, offering seamless integration with async/await and providing a rich set of utilities designed for real-world TypeScript applications.
We welcome contributions to Fates! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated. Please check our Contribution Guidelines for more information.
Fates is licensed under the ISC License. See the LICENSE file for details.
Embrace the power of Fates and elevate your TypeScript projects to new heights of reliability and expressiveness!