Either<L, R>
Either is used for computations that may fail. It is either a Left<L> (error) or a Right<R> (success).
| import { Right } from 'holo-fn/either'
const result = new Right(10)
.map(n => n * 2)
.unwrapOr(0)
console.log(result); // 20
|
Methods
map(fn: (value: R) => U): Either<L, U>
Maps over the Right value. Does nothing for Left.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(10, 2)
.map(n => n * 2)
.unwrapOr(0);
console.log(result1); // 10
const result2 = calculate(10, 0)
.map(n => n * 2)
.unwrapOr(0);
console.log(result2); // 0
|
mapLeft<M>(fn: (err: L) => M): Either<M, R>
Maps over the Left value. Does nothing for Right.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(10, 2)
.map(n => n * 2)
.mapLeft(e => console.log(`Error: ${e}`)) // No printing here
.unwrapOr(0);
console.log(result1); // 10
const result2 = calculate(10, 0)
.map(n => n * 2)
.mapLeft(e => console.log(`Error: ${e}`)) // Prints "Error: Division by zero"
.unwrapOr(0);
console.log(result2); // 0
|
chain(fn: (value: R) => Either<L, U>): Either<L, U>
Chains the transformation if the value is Right. Returns Left otherwise.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(12, 2)
.chain(n => n > 5 ? new Right(n * 2) : new Left("Result is too small"))
.map(n => n + 1)
.mapLeft(e => console.log(`Error: ${e}`)) // Not run
.unwrapOr(0);
console.log(result1); // 13
const result2 = calculate(10, 2)
.chain(n => n > 5 ? new Right(n * 2) : new Left("Result is too small"))
.map(n => n + 1)
.mapLeft(e => console.log(`Error: ${e}`)) // Prints "Error: Result is too small"
.unwrapOr(0);
|
validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R>
Validates the Right value based on a predicate. If the predicate returns true, keeps the value. If it returns false, converts to Left with the provided error. Does nothing for Left.
| import { Left, Right } from 'holo-fn/either';
const result1 = new Right<string, number>(25).validate((n) => n >= 18, 'Must be 18+');
console.log(result1.unwrapOr(0)); // 25
const result2 = new Right<string, number>(15).validate((n) => n >= 18, 'Must be 18+');
console.log(result2.isLeft()); // true
console.log(result2.unwrapOr(0)); // 0
const result3 = new Left<string, number>('Already failed').validate((n) => n >= 18, 'Must be 18+');
console.log(result3.isLeft()); // true (keeps original error)
|
unwrapOr(defaultValue: R): R
Returns the value of Right, or the default value for Left.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(12, 2).unwrapOr(0);
console.log(result1); // 6
const result2 = calculate(10, 0).unwrapOr(-1);
console.log(result2); // -1
|
isRight(): boolean
Checks if the value is Right.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(12, 2).isRight();
console.log(result1); // true
const result2 = calculate(10, 0).isRight();
console.log(result2); // false
|
isLeft(): boolean
Checks if the value is Left.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(12, 2).isLeft();
console.log(result1); // false
const result2 = calculate(10, 0).isLeft();
console.log(result2); // true
|
match<T>(cases: { left: (left: L) => T; right: (right: R) => T }): T
Matches the value to execute either the left or right case.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | import { Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(12, 2)
.chain(n => n > 5 ? new Right(n * 2) : new Left("Result is too small"))
.map(n => n + 1)
.match({
right: n => n,
left: e => {
console.log(`Error: ${e}`); // Not run
return 0;
}
});
console.log(result1); // 13
const result2 = calculate(10, 2)
.chain(n => n > 5 ? new Right(n * 2) : new Left("Result is too small"))
.map(n => n + 1)
.match({
right: n => n,
left: e => {
console.log(`Error: ${e}`); // Prints "Error: Result is too small"
return 0;
}
});
console.log(result2); // 0
|
equals(other: Either<L, R>): boolean
Compares this to another Either, returns false if the values inside are different.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | import { type Either, Left, Right } from "holo-fn/either";
const calculate = (a: number, b: number): Either<string, number> => {
if (b === 0) {
return new Left("Division by zero");
}
return new Right(a / b);
};
const result1 = calculate(12, 2)
.chain(n => n > 5 ? new Right(n * 2) : new Left("Result is too small"))
.map(n => n + 1);
console.log(result1.equals(new Right(13))); // true
const result2 = calculate(10, 2)
.chain(n => n > 5 ? new Right(n * 2) : new Left("Result is too small"))
.map(n => n + 1);
console.log(result2.equals(new Right(0))); // false
|
Helpers
left<L, R = never>(value: L): Either<L, R>
Creates a Left value, representing an error or failure in an operation.
| import { left } from 'holo-fn/either';
const eitherValue = left<string, string>("Error");
console.log(eitherValue.unwrapOr("No error")); // "No error"
|
right<L, R>(value: R): Either<L, R>
Creates a Right value, representing a success in an operation.
| import { right } from 'holo-fn/either';
const eitherValue = right(10);
console.log(eitherValue.unwrapOr(0)); // 10
|
tryCatch(fn, onError?)
Wraps a potentially throwing function in an Either.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | import { tryCatch } from 'holo-fn/either';
const input = '{"name": "John Doe"}'
interface User {
name: string;
}
const convertToJson = (obj: unknown): User => {
if (typeof obj === 'object' && obj !== null && 'name' in obj) {
return obj as User;
}
return { name: 'anonymous' };
}
const parsed = tryCatch(() => JSON.parse(input), e => 'Invalid JSON')
.map(convertToJson)
.unwrapOr({ name: 'anonymous 1' });
console.log(parsed.name) // John Doe
|
- Returns
Right<R> if fn() succeeds
- Returns
Left<L> if it throws, using onError if provided
fromPromise(promise, onError?)
Wraps a Promise<T> into a Promise<Either<L, R>>.
| import { fromPromise } from 'holo-fn/either'
const result = await fromPromise(fetch('/api'), e => 'Network error')
console.log(result) // _Left { value: 'Network error' }
|
- Resolves to
Right<R> on success
- Resolves to
Left<L> on failure
fromAsync(fn, onError?)
Same as fromPromise, but lazy — receives a function returning a Promise.
| import { fromAsync } from 'holo-fn/either'
const result = await fromAsync(async () => await fetch('/api'), e => 'Request failed')
console.log(result) // _Left { value: 'Request failed' }
|
- Allows deferred execution
- Handles exceptions from
async () => ...
Curried Helpers
map
Curried version of map for Either. This allows functional composition with pipe.
| import { map, Right } from 'holo-fn/either';
const result = pipe(
new Right(5),
map((x) => x * 2),
(res) => res.unwrapOr(0)
);
console.log(result); // 10
|
mapLeft
Curried version of mapLeft for Either. This allows mapping over the Left value in a functional pipeline.
| import { Left, mapLeft } from 'holo-fn/either';
const result = pipe(
new Left<string, string>("Error"),
mapLeft((e) => `Mapped error: ${e}`),
(res) => res.unwrapOr("No value")
);
console.log(result); // "No value"
|
chain
Curried version of chain for Either. This allows chaining transformations on the Right value of Either, using a functional composition style.
| import { Right, chain } from 'holo-fn/either';
const result = pipe(
new Right(5),
chain((x) => new Right(x + 5)),
(res) => res.unwrapOr(0)
);
console.log(result); // 10
|
validate
Curried version of validate for Either. This allows filtering/validating values in a functional pipeline with custom error values.
1
2
3
4
5
6
7
8
9
10
11
12
13 | import { right, validate, unwrapOr } from 'holo-fn/either';
const validateAge = (age: number) =>
pipe(
right<string, number>(age),
validate((x: number) => x >= 0, 'Age cannot be negative'),
validate((x: number) => x <= 150, 'Age too high'),
validate((x: number) => x >= 18, 'Must be 18+'),
unwrapOr(0)
);
console.log(validateAge(25)); // 25
console.log(validateAge(15)); // 0 (fails validation)
|
Common use cases:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | import { right, tryCatch, validate } from 'holo-fn/either';
// Validate email format
const validateEmail = (email: string) =>
pipe(
right<string, string>(email),
validate((s: string) => s.length > 0, 'Email is required'),
validate((s: string) => s.includes('@'), 'Must contain @'),
validate((s: string) => s.includes('.'), 'Invalid domain')
);
console.log(validateEmail('test@example.com').unwrapOr('Invalid email'));
// Parse and validate numbers
const parsePositive = (input: string) =>
pipe(
tryCatch(
() => parseInt(input, 10),
() => 'Invalid number'
),
validate((n: number) => !isNaN(n), 'Not a number'),
validate((n: number) => n > 0, 'Must be positive')
);
console.log(parsePositive('42').unwrapOr(0));
// Validate with structured errors
type ValidationError = { code: string; message: string };
const validateUser = (age: number) =>
pipe(
right<ValidationError, number>(age),
validate((x: number) => x >= 18, { code: 'AGE_ERROR', message: 'Must be 18+' })
);
console.log(validateUser(20));
console.log(validateUser(15));
|
unwrapOr
Curried version of unwrapOr for Either. This provides a cleaner way to unwrap the value in a Either, returning a default value if it's Left.
| import { Left, unwrapOr } from 'holo-fn/either';
const result = pipe(
new Left<string, string>("Fail"),
unwrapOr("No value")
);
console.log(result); // "No value"
|
match
Curried version of match for Either. This allows handling Left and Right in a functional way.
| import { match, Right } from 'holo-fn/either';
const result = pipe(
new Right<string, number>(10),
match({
left: (e) => `Error: ${e}`,
right: (v) => `Success: ${v}`
})
);
console.log(result); // "Success: 10"
|
equals
Curried version of equals for Either. Compares this to another Either, returns false if the values inside are different.
| import { equals, Right } from 'holo-fn/either';
import { pipe } from 'remeda';
const result = pipe(
new Right(10),
equals(new Right(10))
);
console.log(result); // true
|
all
Combines an array of Either values into a single Either. Returns Right with all values if all are Right, or Left with all errors if any are Left.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | import { all, left, right, type Either } from 'holo-fn/either';
const result1: Either<unknown, number[]> = all([right(1), right(2), right(3)]);
console.log(result1.unwrapOr([])); // [1, 2, 3]
const result2 = all([left('Name required'), left('Email invalid'), right(25)]);
console.log(
result2.match({
left: (e) => e,
right: (v) => v,
})
); // ['Name required', 'Email invalid']
// Empty array
const result3 = all([]);
console.log(result3.unwrapOr([])); // []
|
sequence
Combines an array of Either values into a single Either, stopping at the first error (fail-fast). Returns Right with all values if all are Right, or Left with the first error encountered.
Unlike all which collects all errors, sequence returns immediately when it finds the first Left.
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | import { left, right, sequence, type Either } from 'holo-fn/either';
const result1: Either<unknown, number[]> = sequence([right(1), right(2), right(3)]);
console.log(result1.unwrapOr([])); // [1, 2, 3]
const result2 = sequence([
right(1),
left('First error'),
left('Second error')
]);
console.log(result2.match({
left: (e) => e,
right: (v) => v
})); // 'First error' (not an array!)
|
partition
Separates an array of Either values into two groups: lefts and rights. Always processes all items and returns both arrays.
Unlike all and sequence which return an Either, partition returns a plain object with two arrays.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | import { left, partition, right } from 'holo-fn/either';
const eithers = [
right<string, number>(1),
left('error1'),
right<string, number>(2),
left('error2'),
right<string, number>(3),
];
const { lefts, rights } = partition(eithers);
console.log(rights); // [1, 2, 3]
console.log(lefts); // ['error1', 'error2']
const { lefts: errors, rights: values } = partition(eithers);
console.log(`✓ ${values.length} succeeded`);
console.log(`✗ ${errors.length} failed`);
errors.forEach((err) => console.error(err));
|
Common Patterns
Discriminated union errors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | import { left, match, type Either } from 'holo-fn/either';
import { pipe } from 'rambda';
type User = {
name: string;
email: string;
};
type ValidationError =
| { type: 'INVALID_EMAIL'; email: string }
| { type: 'TOO_SHORT'; minLength: number }
| { type: 'REQUIRED'; field: string };
const result: Either<ValidationError, User> = left({
type: 'INVALID_EMAIL',
email: 'bad@email',
});
const message = pipe(
result,
match({
left: (err) => {
switch (err.type) {
case 'INVALID_EMAIL':
return `Invalid email: ${err.email}`;
case 'TOO_SHORT':
return `Must be at least ${err.minLength} characters`;
case 'REQUIRED':
return `Field ${err.field} is required`;
}
},
right: (user) => `Success: ${user.name}`,
})
);
|
Batch operations with error tracking
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | import { all, left, match, right } from 'holo-fn/either';
import { pipe } from 'rambda';
type AddressData = {
street: string;
city: string;
zip: string;
};
type UserData = {
email: string;
age: number;
address: AddressData;
};
const validateEmail = (email: string) => (email.includes('@') ? right(email) : left('Invalid email'));
const validateAge = (age: number) => (age >= 18 ? right(age) : left('Must be 18+'));
const validateAddress = (address: AddressData) => {
if (!address.street || !address.city || !address.zip) {
return left('Incomplete address');
}
return right(address);
};
const validateForm = (data: UserData) =>
pipe(
all([validateEmail(data.email), validateAge(data.age), validateAddress(data.address)]),
match({
left: (err) => `Validation failed: ${err}`,
right: ([email, age, address]) =>
`Validation succeeded: ${email}, ${age}, ${address.street}, ${address.city}, ${address.zip}`,
})
);
console.log(
validateForm({
email: 'user@example.com',
age: 25,
address: {
street: '123 Main St',
city: 'Anytown',
zip: '12345',
},
})
);
|