This is a set of implementations of monads in TypeScript with OOP perspective.
- monads
- Installation
- Either Monad
- Usage
- Creating an Either
- Creating Either from possible failed operations
- Mapping over an Either
- Using Railway Pattern Methods
- Recovering from a Left value
- Running side effects
- Folding an Either
- checking if an Either is Right or Left
- Chaining operations
- Handling errors
- Asynchronous Operations (AsyncEither)
- Usage
- Option Monad
- Try Monad
- Future Monad
- IO Monad
You can install the package using npm:
npm install @leanmind/monads
The Either
monad represents a value of one of two possible types (a disjoint union).
An Either
is either a Left
or a Right
.
By convention, Right
is used to hold a successful value,
while Left
is used to hold an error or failure.
You can create an Either
using the static methods Either.right
and Either.left
.
import { Either } from '@leanmind/monads';
// Creating a Right
const right = Either.right(42);
// Creating a Left
const left = Either.left('Error');
You can create an Either
from a failed operations using the static method Either.catch
.
import { Either } from '@leanmind/monads';
const findUser = (id: number): User => {
if (id === 42) {
return { id: 42, name: 'John Doe' };
}
throw new Error('User with id ${id} not found');
};
const right = Either.catch<User>(() => findUser(42)); // Right({ id: 42, name: 'John Doe' })
const left = Either.catch<User>(() => findUser(1)); // Left(Error('User with id 1 not found'))
You can use the flatMap
or map
method to transform the value inside a Right
, and flatMapLeft
or mapLeft
to
transform the value inside a Left
.
import { Either } from '@leanmind/monads';
const right = Either.right(42).flatMap(x => Either.right(x + 1)); // Right(43)
const left = Either.left('Error').flatMapLeft(err => Either.left(`New ${err}`)); // Left('New Error')
import { Either } from '@leanmind/monads';
const right = Either.right(42).map(x => x + 1); // Right(43)
const left = Either.left('Error').mapLeft(err => `New ${err}`); // Left('New Error')
You can use andThen
and orElse
methods which follow the Railway-oriented programming pattern. These methods are
semantically equivalent to flatMap
and flatMapLeft
but offer more readable syntax for error handling flows.
import { Either } from '@leanmind/monads';
// Using andThen to chain operations on successful values (Right)
const right = Either.right(42)
.andThen(x => Either.right(x + 1)); // Right(43)
// Using orElse to handle errors (Left)
const left = Either.left('Error')
.orElse(err => Either.left(`Handled: ${err}`)); // Left('Handled: Error')
// Chaining operations with Railway methods
const result = Either.right(42)
.andThen(x => {
if (x > 40) {
return Either.right(x + 1);
}
return Either.left('Value too small');
})
.orElse(err => Either.left(`Error: ${err}`)); // Right(43)
You can use combineWith
to combine multiple Either instances into one that contains a tuple of their values. This is
useful for collecting multiple validations or operations that could fail.
import { Either } from '@leanmind/monads';
class Name {
private constructor(private value: string) {
}
static of(value: string): Either<string, Name> {
return value.length >= 2
? Either.right(new Name(value))
: Either.left('Name must be at least 2 characters long');
}
}
class Email {
private constructor(private value: string) {
}
static of(value: string): Either<string, Email> {
return value.includes('@')
? Either.right(new Email(value))
: Either.left('Email must contain @ symbol');
}
}
class Age {
private constructor(private value: number) {
}
static of(value: number): Either<string, Age> {
return value >= 18
? Either.right(new Age(value))
: Either.left('Age must be at least 18');
}
}
class Address {
private constructor(private value: string) {
}
static of(value: string): Either<string, Address> {
return value.length > 5
? Either.right(new Address(value))
: Either.left('Address must be longer than 5 characters');
}
}
// Class that requires all validated fields
class Account {
constructor(
public name: Name,
public email: Email,
public age: Age,
public address: Address,
) {}
}
// Combine all validations and create account if all are successful
const maybeAccount = Name.of('John')
.combineWith<[Email, Age, Address]>([
Email.of('john@mail.com'),
Age.of(37),
Address.of('Main St., 123')
])
.map(([name, email, age, address]) => new Account(name, email, age, address));
// Result: Right(Account{...})
// If any validation fails, the result will be Left with the first error
const failedAge = Name.of('John')
.combineWith<[Email, Age, Address]>([
Email.of('john@mail.com'),
Age.of(16), // This will fail
Address.of('Main St., 123'),
])
.map(([name, email, age, address]) => new Account(name, email, age, address));
// Result: Left('Age must be at least 18')
You can use the recover
method to recover from a Left
value and transform it into a Right
.
import { Either } from '@leanmind/monads';
const recoverIfEven = (x: number) => {
if (x % 2 === 0) {
return Either.right('Even');
}
return Either.left('Not even');
};
const right = Either.right<number, string>('irrelevant').recover(recoverIfEven); // Right('irrelevant')
const leftEven = Either.left<number, number>(42).recover(recoverIfEven); // Right('Even')
const leftOdd = Either.left<number, number>(43).recover(recoverIfEven); // Left('Not even')
You can use the onRight
method to run side effects on the value inside a Right
.
import { Either } from '@leanmind/monads';
const right = Either.right(42).onRight(x => console.log(x)); // 42
const left = Either.left('Error').onRight(x => console.log(x)); // No execution
Or you can use the onLeft
method to run side effects on the value inside a Left
.
import { Either } from '@leanmind/monads';
const right = Either.right(42).onLeft(err => console.log(err)); // No execution
const left = Either.left('Error').onLeft(err => console.log(err)); // 'Error'
You can use the fold
method to handle both Right
and Left
cases and unwrap the result.
import { Either } from '@leanmind/monads';
const success = Either.right<string, number>(42).fold({
ifRight: x => `${x + 1}`,
ifLeft: err => `Error: ${err}`,
}); // '43'
const error = Either.left<string, number>('Error').fold({
ifRight: x => `${x + 1}`,
ifLeft: err => `Error: ${err}`,
}); // 'Error: Error'
You can check explicitly if an Either
is Right
or Left
using the isRight
and isLeft
methods.
Probably you will not need to use these methods, but they are available
in case of refactoring from try-catch blocks or other situations.
import { Either } from '@leanmind/monads';
const right = Either.right(42);
const left = Either.left('Error');
right.isRight(); // true
right.isLeft(); // false
left.isRight(); // false
left.isLeft(); // true
You can chain operations using the map
, mapLeft
, flatMap
and flatMapLeft
method.
The following example demonstrates how to chain operations using the map method:
import { Either } from '@leanmind/monads';
const result = Either.right(42)
.map(x => x + 1)
.map(x => x * 2)
.fold({
ifRight: x => x.toString(),
ifLeft: err => `Error: ${err}`,
})
console.log(result); // 86
Here is a complete example demonstrating the usage of the Either
monad:
import { Either } from '@leanmind/monads';
function divide(a: number, b: number): Either<string, number> {
if (b === 0) {
return Either.left('Division by zero');
}
return Either.right(a / b);
}
const result = divide(10, 2)
.map(x => x * 2)
.fold({
ifRight: x => `Result: ${x}`,
ifLeft: err => `Error: ${err}`,
});
console.log(result); // 'Result: 10'
In this example, the divide function returns an Either
that represents the result of the division or an error if the
division is by zero. The result is then transformed and folded to produce a final string
.
AsyncEither
is the asynchronous variant of Either
, which wraps a Promise that resolves to an Either. It provides
similar functionality to synchronous Either
but works with asynchronous operations.
You can create an AsyncEither
using the static methods AsyncEither.fromPromise
, AsyncEither.fromSafePromise
, and
AsyncEither.fromSync
.
import { AsyncEither, Either } from '@leanmind/monads';
// Creating an AsyncEither from a Promise with error handling
const fromPromise = AsyncEither.fromPromise(
fetch('https://api.example.com/users/1').then(res => res.json()),
err => `API Error: ${err}`
); // AsyncEither<string, User>
// Creating an AsyncEither from a Promise that cannot fail
const fromSafePromise = AsyncEither.fromSafePromise(
Promise.resolve(42)
); // AsyncEither<never, number>
// Converting a synchronous Either to an AsyncEither
const fromSync = AsyncEither.fromSync(Either.right(42)); // AsyncEither<never, number>
Similar to Either
, you can use map
, mapLeft
, flatMap
, and flatMapLeft
methods to transform values
asynchronously:
import { AsyncEither, Either } from '@leanmind/monads';
// Using map
const mapped = await AsyncEither.fromSync(Either.right(42))
.map(x => x * 2); // AsyncEither<never, 84>
// Using mapLeft
const mappedLeft = await AsyncEither.fromSync(Either.left('error'))
.mapLeft(err => `Transformed: ${err}`); // AsyncEither<'Transformed: error', never>
// Using flatMap with async operations
const flatMapped = await AsyncEither.fromSync(Either.right(42))
.flatMap(x => AsyncEither.fromPromise(
Promise.resolve(x + 1),
err => `Error: ${err}`
)); // AsyncEither<string, 43>
// Using flatMapLeft
const flatMappedLeft = await AsyncEither.fromSync(Either.left('error'))
.flatMapLeft(err => AsyncEither.fromSync(Either.left(`${err}_handled`))); // AsyncEither<'error_handled', never>
Note that async transformations are supported for both map and flatMap operations:
import { AsyncEither } from '@leanmind/monads';
const asyncMapped = await AsyncEither.fromSync(Either.right(42))
.map(async x => {
const result = await someAsyncOperation(x);
return result * 2;
}); // AsyncEither<never, number>
Similar to synchronous Either, AsyncEither also supports Railway-oriented programming with andThen
and orElse
methods:
import { AsyncEither, Either } from '@leanmind/monads';
// Using andThen with AsyncEither
const result = await AsyncEither.fromSync(Either.right(42))
.andThen(x => AsyncEither.fromSync(Either.right(x + 1)))
.fold({
ifRight: x => `Result: ${x}`,
ifLeft: err => `Error: ${err}`
}); // 'Result: 43'
// Using orElse to handle errors in async processing
const handleError = await AsyncEither.fromSync(Either.left('Network error'))
.orElse(err => AsyncEither.fromSync(Either.right(`Recovered from ${err}`)))
.fold({
ifRight: x => `Success: ${x}`,
ifLeft: err => `Failed: ${err}`
}); // 'Success: Recovered from Network error'
// Real-world example with API call
async function fetchUserData(userId: string) {
return AsyncEither.fromPromise(
fetch(`https://api.example.com/users/${userId}`),
error => `Failed to fetch user: ${error.message}`
)
.andThen(response => {
if (!response.ok) {
return AsyncEither.fromSync(Either.left(`HTTP error: ${response.status}`));
}
return AsyncEither.fromPromise(
response.json(),
error => `Failed to parse response: ${error.message}`
);
})
.andThen(user => {
if (!user.id) {
return AsyncEither.fromSync(Either.left('Invalid user data'));
}
return AsyncEither.fromSync(Either.right(user));
})
.orElse(error => {
console.error(`API error: ${error}`);
return AsyncEither.fromSync(Either.left(`Friendly error: Something went wrong`));
});
}
While not explicitly shown in the provided code, you can use the fold
method with appropriate handlers to perform side
effects:
import { AsyncEither, Either } from '@leanmind/monads';
const asyncEither = AsyncEither.fromSync(Either.right(42));
// Execute side effects after resolving the AsyncEither
await asyncEither.then(either => {
either.onRight(value => console.log(`Success: ${value}`)); // Logs "Success: 42"
either.onLeft(error => console.error(`Error: ${error}`)); // Not executed
});
You can use the fold
method to handle both Right
and Left
cases and unwrap the result:
import { AsyncEither, Either } from '@leanmind/monads';
const asyncEither = AsyncEither.fromSync(Either.right(42));
const result = await asyncEither.fold({
ifRight: x => `Success: ${x}`,
ifLeft: err => `Error: ${err}`,
}); // 'Success: 42'
const asyncEitherError = AsyncEither.fromSync(Either.left('failed'));
const errorResult = await asyncEitherError.fold({
ifRight: x => `Success: ${x}`,
ifLeft: err => `Error: ${err}`,
}); // 'Error: failed'
AsyncEither
implements the PromiseLike
interface, allowing it to be used in Promise chains and with await
:
import { AsyncEither, Either } from '@leanmind/monads';
// Using await to get the wrapped Either
const asyncEither = AsyncEither.fromSync(Either.right(42));
const either = await asyncEither; // Either<never, 42>
// Using in Promise chain
AsyncEither.fromPromise(fetchUser(1), err => `Failed to fetch: ${err}`)
.then(either => {
either.fold({
ifRight: user => console.log(`User: ${user.name}`),
ifLeft: err => console.error(err)
});
});
Here's a complete example of handling asynchronous operations with error handling:
import { AsyncEither } from '@leanmind/monads';
async function fetchUserData(userId: string) {
// Create an AsyncEither from a Promise that might fail
return AsyncEither.fromPromise(
fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}),
error => `Failed to fetch user: ${error.message}`
);
}
// Usage
async function displayUserInfo(userId: string) {
const userResult = await fetchUserData(userId)
.map(user => ({
displayName: `${user.firstName} ${user.lastName}`,
email: user.email
}))
.fold({
ifRight: userInfo => `User: ${userInfo.displayName} (${userInfo.email})`,
ifLeft: error => `Error: ${error}`
});
console.log(userResult);
}
displayUserInfo('123'); // Either 'User: John Doe (john@example.com)' or 'Error: Failed to fetch user: ...'
This example demonstrates how AsyncEither
helps with handling asynchronous operations that might fail, allowing for
clean error handling and functional transformations of the results.
The Option
monad represents a value that may or may not be present.
An Option
is either a Some
or a None
.
Some
is used to hold a value, while None
is used to represent the absence of a value.
You can create an Option
using the static methods Option.of
.
import { Option } from '@leanmind/monads';
// Creating a Some
const some = Option.of(42); // Some(42)
// Creating a None
const none = Option.of(null); // None
You can use the getOrElse
method to retrieve the value of an Option
or provide a default value if it is None
.
import { Option } from '@leanmind/monads';
const some = Option.of(42);
some.getOrElse(0); // 42
const none = Option.of(null);
none.getOrElse(0); // 0
You can use the filter
method to keep the Some
value if it satisfies a predicate.
import { Option } from '@leanmind/monads';
const some = Option.of(42).filter(x => x > 40); // Some(42)
const none = Option.of(42).filter(x => x > 50); // None
You can use the flatMap
or map
method to transform the Some
value.
import { Option } from '@leanmind/monads';
const some = Option.of(42).flatMap(x => Option.of(x + 1)); // Some(43)
const none = Option.of(null).flatMap(x => Option.of(x + 1)); // None
import { Option } from '@leanmind/monads';
const some = Option.of(42).map(x => x + 1); // Some(43)
const none = Option.of(null).map(x => x + 1); // None
Option also supports Railway-oriented programming with andThen
and orElse
methods, which provide a clean way to
chain operations:
import { Option } from '@leanmind/monads';
// Using andThen with Option
const result = Option.of(42)
.andThen(x => Option.of(x + 1)); // Some(43)
// Using orElse to provide an alternative for None
const none = Option.of<number>(null)
.orElse(() => Option.of(42)); // Some(42)
// Chaining operations
const validationResult = Option.of('test@example.com')
.andThen(email => {
if (email.includes('@')) {
return Option.of(email);
}
return Option.none();
})
.orElse(() => Option.of('default@example.com'));
You can use combineWith
to combine multiple Option instances into one that contains a tuple of their values. This is
useful when you need all values to be present to proceed.
import { Option } from '@leanmind/monads';
// User profile information that may be incomplete
const username = Option.of('johndoe');
const email = Option.of('john@example.com');
const age = Option.of(30);
const address = Option.of('123 Main St');
// Combine all fields to create a complete profile
const completeProfile = username
.combineWith<[string, number, string]>([email, age, address])
.map(([name, mail, years, addr]) => ({
username: name,
email: mail,
age: years,
address: addr
}));
// If all fields are present: Some({ username: 'johndoe', email: 'john@example.com', age: 30, address: '123 Main St' })
// If any field is missing, the result will be None
const incompleteProfile = username
.combineWith<[string, number, string]>([
email,
Option.of(undefined), // Missing age
address
])
.map(([name, mail, years, addr]) => ({
username: name,
email: mail,
age: years,
address: addr
}));
// Result: None
You can use the onSome
method to run side effects on the value inside a Some
.
import { Option } from '@leanmind/monads';
const some = Option.some(42).onSome(x => console.log(x)); // 42
const none = Option.none().onSome(x => console.log(x)); // No execution
Or you can use the onNone
method to run side effects on the value inside a None
.
import { Option } from '@leanmind/monads';
const some = Option.some(42).onNone(_ => console.log('Empty value')); // No execution
const none = Option.none().onNone(_ => console.log('Empty value')); // 'Empty value'
You can use the fold
method to handle both Some
and None
cases and unwrap the result.
import { Option } from '@leanmind/monads';
const some = Option.of(42).fold({
ifSome: x => `${x + 1}`,
ifNone: () => 'No value',
}); // '43'
const none = Option.of(null).fold({
ifSome: x => `${x + 1}`,
ifNone: () => 'No value',
}); // 'No value'
If needed, you can check explicitly if an Option
is Some
or None
using the isSome
and isNone
methods.
import { Option } from '@leanmind/monads';
const some = Option.of(42);
some.isSome(); // true
some.isNone(); // false
const none = Option.of(undefined);
none.isSome(); // false
none.isNone(); // true
The Try
monad represents a computation that may fail.
you can create a Try
using the static method Try.success
or Try.failure
.
import { Try } from '@leanmind/monads';
const success = Try.success(42); // Success(42)
const failure = Try.failure(new Error('Error')); // Failure(Error('Error'))
Also, you can create a Try
using the static method Try.execute
from a function that may throw an exception.
import { Try } from '@leanmind/monads';
const success = Try.execute(() => 42); // Success(42)
const failure = Try.execute(() => {
throw new Error('Error');
}); // Failure(Error('Error'))
You can use the map
method to transform the value inside a Success
.
import { Try } from '@leanmind/monads';
const success = Try.success(42).map(x => x + 1); // Success(43)
You can use the flatMap
method to transform the value inside a Success
with a fallible closure.
import { Try } from '@leanmind/monads';
const success = Try.success(42).flatMap(x => Try.success(x + 1)); // Success(43)
Try also supports Railway-oriented programming with andThen
and orElse
methods, which provide a clean way to handle
success and error cases:
import { Try } from '@leanmind/monads';
// Using andThen to chain successful operations
const result = Try.execute(() => JSON.parse('{"key": "value"}'))
.andThen(obj => Try.success(obj.key)); // Success('value')
// Using orElse to recover from failures
const recoveredResult = Try.execute(() => JSON.parse('invalid json'))
.orElse(error => Try.success({ error: error.message })); // Success({ error: '...' })
// Chaining operations
const parseConfig = Try.execute(() => JSON.parse('{"port": 8080}'))
.andThen(config => {
if (config.port) {
return Try.success(`Server will run on port ${config.port}`);
}
return Try.failure(new Error('Port configuration missing'));
})
.orElse(_ => Try.success('Server will run on default port 3000'));
// Result: Success('Server will run on port 8080')
You can use combineWith
to combine multiple Try instances into one that contains a tuple of their values. This is
useful for operations that should all succeed or return the first error:
import { Try } from '@leanmind/monads';
// Database operations that may fail
const fetchUser = Try.execute(() => ({ id: 1, name: 'John' }));
const fetchPosts = Try.execute(() => [{ title: 'Hello World' }]);
const fetchComments = Try.execute(() => [{ text: 'Great post!' }]);
// Combine all operations to get user data with posts and comments
const userData = fetchUser
.combineWith<[Array<{ title: string }>, Array<{ text: string }>]>([fetchPosts, fetchComments])
.map(([user, posts, comments]) => ({
user,
posts,
comments,
summary: `User ${user.name} has ${posts.length} posts and ${comments.length} comments`
}));
// If all operations succeed:
// Success({ user: { id: 1, name: 'John' }, posts: [{ title: 'Hello World' }], comments: [{ text: 'Great post!' }], summary: 'User John has 1 posts and 1 comments' })
// If any operation fails, the result will contain the first error
const failingOperation = fetchUser
.combineWith<[Array<{ title: string }>, Array<{ text: string }>]>([
Try.failure(new Error('Failed to fetch posts')),
fetchComments
])
.map(([user, posts, comments]) => ({
user,
posts,
comments
}));
// Result: Failure(Error('Failed to fetch posts'))
You can use the onSuccess
method to run side effects on the value inside a Success
.
import { Try } from '@leanmind/monads';
const succcess = Try.succcess(42).onSuccess(x => console.log(x)); // 42
const failure = Try.failure('Error').onSuccess(x => console.log(x)); // No execution
Or you can use the onFailure
method to run side effects on the value inside a Failure
.
import { Try } from '@leanmind/monads';
const succcess = Try.succcess(42).onFailure(err => console.log(err)); // No execution
const failure = Try.failure(new Error('Error')).onFailure(err => console.log(err)); // Error('Error')
You can use the getOrElse
method to retrieve the value of a Success
or provide a default value if it is Failure
.
import { Try } from '@leanmind/monads';
const success = Try.success(42);
const value = success.getOrElse(0); // 42
const failure = Try.failure(new Error('Error'));
const otherValue = failure.getOrElse(0); // 0
Also, you can use the getOrThrow
method to retrieve the value of a Success
or throw the error if it is Failure
.
import { Try } from '@leanmind/monads';
const success = Try.success(42);
const value = success.getOrThrow(); // 42
const failure = Try.failure(new Error('Error'));
const otherValue = failure.getOrThrow(); // throws Error('Error')
You can use the fold
method to handle both Success
and Failure
cases and unwrap the result.
import { Try } from '@leanmind/monads';
const success = Try.success(42).fold({
ifSuccess: x => `${x + 1}`,
ifFailure: err => `Error: ${err}`,
}); // '43'
const failure = Try.failure(new Error('an error')).fold({
ifSuccess: x => `${x + 1}`,
ifFailure: err => `Error: ${err}`,
}); // 'Error: an error'
Normally, Try is used to handle Exceptions
that are raise by third party libraries
import { Try } from '@leanmind/monads';
const result = Try.execute(() => {
// Some API of a library that may throw an exception
return 42;
}).fold({
ifSuccess: x => `${x + 1}`,
ifFailure: err => `Error: ${err.message}`,
})
console.log(result); // 43
If needed, you can check explicitly if a Try
is Success
or Failure
using the isSuccess
and isFailure
methods.
import { Try } from '@leanmind/monads';
const success = Try.execute(() => 42);
success.isSuccess(); // true
const failure = Try.execute(() => {
throw new Error('Error');
});
failure.isFailure(); // true
The Future
monad represents a computation that may be executed asynchronously.
You can create a Future
using the static method Future.of
.
import { Future } from '@leanmind/monads';
const future = Future.of(() => Promise.resolve(42));
You can use the map
or flatMap
method to transform the computed value inside a Future
. The operation will not
execute the transformation (lazy evaluation) until complete
method is called.
import { Future } from '@leanmind/monads';
const future = Future.of(() => Promise.resolve(42))
.flatMap(x => Future.of(() => Promise.resolve(x + 1)))
.complete(
x => console.log(x),
err => console.error(err)
); // 43
import { Future } from '@leanmind/monads';
const future = Future.of(() => Promise.resolve(42))
.map(x => x + 1)
.complete(
x => console.log(x),
err => console.error(err)
); // 43
You can evaluate a Future
using the complete
method. The complete
method takes two functions as arguments:
one for the success case and one for the failure case.
import { Future } from '@leanmind/monads';
const successFuture = Future.of(() => Promise.resolve(42));
await successFuture.complete(
x => console.log(x),
err => console.error(err)
); // 42
const failureFuture = Future.of(() => Promise.reject(new Error('Error')));
await failureFuture.complete(
x => console.log(x),
err => console.error(err)
); // Error('Error')
The IO
monad represents a computation that may have side effects.
In this way, the IO
monad is used to encapsulate side effects in a pure functional way.
So, you can operate as pure functions until you call the runUnsafe
method.
You can create an IO
using the static method IO.of
.
import { IO } from '@leanmind/monads';
const io = IO.of(() => 42);
You can use the flatMap
or map
method to concatenate IO
operations.
The operation is not executed until you call the runUnsafe
method.
import { IO } from '@leanmind/monads';
const io = IO.of(() => 42).flatMap(x => IO.of(() => x + 1));
io.run(); // 43
import { IO } from '@leanmind/monads';
const io = IO.of(() => 42).map(x => x + 1);
io.runUnsafe(); // 43