This library is super-new and therefore highly liable to change. That's the downside. The upside is that if you have ideas for improvements, I can easily add them! Raise an issue in Github and I'll take a look.
All types inherit from Perhaps
, which is a catch-call container for values which may not exist or be erroneous. There are three subtypes:
Something
for values that do existNone
for values that don'tProblem
for errors
You can query the type of a Perhaps using instanceof
:
const isSomething = perhaps instanceof Something;
const isNothing = perhaps instanceof None;
const isProblem = perhaps instanceof Problem;
There is also a helper value Nothing
- this is a single instance of None
. All Nones are functionally identical, so it's handy to have a single value for use in comparisons etc., e.g.
const isNothing = perhaps === Nothing;
Static method.
Perhaps.of(T) = Something<T> | Problem | Nothing
;
Creates a new Perhaps
(Something
or Nothing
) using the provided value, according to the following rules:
- If the value is null, undefined or an empty string, Perhaps.of returns
Nothing
- If the value is another
Perhaps
, that will be returned directly - Otherwise, returns a
Something
e.g.
let a: Perhaps<number> = Perhaps.of(123);
let b: None = Perhaps.of(null | undefined | '');
let c: Perhaps<string> = Perhaps.of( Perhaps.of('hello') );
Static method.
Perhaps.from(Function) = Something<T> | Problem | Nothing
;
Creates a new Perhaps
from the provided function, according to the following rules:
- If the function returns a null, undefined or empty string, Perhaps.from returns
Nothing
- If the function returns another
Perhaps
, that will be returned directly - If the function throws an error, Perhaps.from returns a
Problem
wrapping the exception - Otherwise, returns a
Something
e.g.
let something = Perhaps.from(()=> 123);
let nothing = Perhaps.from(()=> null);
let problem = Perhaps.from(()=> {throw new Error});
Static method.
Perhaps.junction(Perhaps, Perhaps, Perhaps... Function) = Something | Problem | Nothing
Given n input Perhaps objects and one combination function, tries to construct a new Perhaps using the values wrapped in the inputs. If any of the inputs are Nothing
or a Problem
, then Nothing
or the Problem
are returned.
Imagine you have a user object (which may or may not be null) with firstName and lastName fields (which may also be or not be null). Junction lets you wrap both values in a perhaps, then construct a new perhaps when and only when both inputs are valid, as so:
const firstName = Perhaps.of(user).map(user => user.firstName);
const lastName = Perhaps.of(user).map(user => user.lastName);
const fullName = Perhaps.junction(firstName, lastName, (first: string, last: string) => first + ' ' + last);
The result of the combination function is passed through Perhaps.of
before being returned, so functions are allowed to spit out null, undefineds, wrapped Perhaps-es or even throw errors.
Instance method.
Perhaps.of(input).catch(Error => T | void) = Something<T> | Nothing | Problem
If the Perhaps is a Problem
- meaning it wraps an error - this method will call the passed function with that error. The function may either return a value (which will create a Something or Nothing) or throw again (creating another Problem). It's a lot like promise.catch
.
Note that the function just won't be called if the Perhaps is Something or Nothing.
If a wrapped error in a Problem isn't handled by catch
, it will get thrown the next time the Problem is unwrap
ped.
e.g.
const userName = Perhaps
.of(localStorage.getItem('user'))
.map(json => JSON.parse(json)) // might throw error
.map(user => user.name) // ignored if Problem
.catch(e => {
console.error(e);
return 'Invalid user'; // omit return to create a Nothing
});
Instance method.
Perhaps.of(input).forOne(T => void) = Something<T> | Problem
If the Perhaps is a Something (not Nothing or a Problem), calls the passed function with the inner value, and discard the result.
This method will either return the original Perhaps
or, if the passed function throws an error, a Problem
wrapping that error.
let a: Perhaps<any> = Perhaps.of(null).forOne(willNotBeCalled);
let b: Perhaps<number> = Perhaps.of(1234).forOne(willBeCalled);
Instance method.
Perhaps.of(input).map(T => U | Perhaps<U>) = Something<U> | Nothing | Problem
If the Perhaps is a Something (not Nothing or a Problem), calls the passed function with the inner value, and returns the result wrapped in a new Perhaps.
If the map function returns an empty value or throws an error, the return value will be Nothing or a Problem respectively.
Finally, if a map function itself returns a Perhaps
, the result will be automatically 'flattened' - Perhaps objects will never contain another Perhaps inside.
Note that after mapping, the original Perhaps is unchanged (all Perhaps objects are immutable).
Examples:
const something: Perhaps<number> = Perhaps.of(100);
const nothing: None = Perhaps.of(null);
const problem: Problem = Perhaps.from(()=> new Error);
function double(x: number) {
return x * 2;
}
something.map(double); // is a Something wrapping 200
nothing.map(double); // is a Nothing. Double was not called.
problem.map(double); // is a Problem. Double was not called.
function returnEmpty(x: number) {
return undefined;
}
something.map(returnEmpty); // is a Nothing
nothing.map(returnEmpty); // still a Nothing. ReturnEmpty was not called.
problem.map(returnEmpty); // still a Problem. ReturnEmpty was not called.
function returnSomething(x: number) {
return Perhaps.of(number + 1);
}
something.map(returnSomething); // is a Something wrapping 101
nothing.map(returnSomething); // still a Nothing. ReturnSomething was not called.
problem.map(returnSomething); // still a Problem. ReturnSomething was not called.
function returnNothing(x: number) {
return Perhaps.of(null);
}
something.map(returnNothing); // is a Nothing
Instance method.
Perhaps.from(fn).or(T | null) = Something<T> | Nothing | Problem
If the Perhaps is a Nothing, attempt to construct a new Perhaps using an alternate value. If the alternate value is 'empty' (null / undefined / an empty string), the result will still be Nothing.
Instance method.
Perhaps.from(fn).orFrom(()=> T | null) = Something<T> | Nothing | Problem
If the Perhaps is a Nothing, attempt to construct a new Perhaps using the provided function. If the function returns an empty value, the result will be Nothing again. If the function throws an exception, the result will be a Problem.
Instance method.
Perhaps.from(fn).peek() = T | null | Error
Unwraps the value for inspection, debugging etc. Problem
s will unwrap to their Error
objects; Nothing
and None
s unwrap to null, even if they were created by other empty values (e.g. undefined).
You probably want to use unwrap instead.
Instance method.
Perhaps.from(fn).unwrap() = T | null | <throws error>
Unwraps the value for use, following these rules:
- If the Perhaps is a Something, will unwrap to its value
- If the Perhaps is a Nothing, will unwrap to
null
- If the Perhaps is a Problem, will throw its Error
Note that in TypeScript, you can narrow the class of a Perhaps using instanceof checks:
if (myPerhaps instanceof Something) {
myPerhaps.unwrap(); // typescript will know this value cannot be null
}
Instance method.
Perhaps.from(fn).unwrapOr(T) = T | null | <throws error>
Equivalent to calling perhaps.or
and then perhaps.unwrap
.
Instance method.
Perhaps.from(fn).unwrapOrThrow(Error) = T | null | <throws provided error>
Unwraps the value for use, following these rules:
- If the Perhaps is a Something, will unwrap to its value
- If the Perhaps is a Nothing, will throw provided error
- If the Perhaps is a Problem, will throw provided error
An instance of None
. All Nones are functionally identical, so it's simpler and more efficient to use just a singleton.
Represents a Perhaps that wraps an empty value.
"Empty values" are:
- null
- undefined
- the empty string
''
- the number
NaN
Represents a Perhaps that wraps an exception. Used mainly to handle errors thrown by mapping functions.
Can be constructed directly with an Error object:
const myProblem = new Problem(new Error('Oh no!'));