tupperware
A library for safely and consistently dealing with complex values in Javascript and Typescript.
Overview
This library provides two types for dealing with optional / conditional values:
- Optional - A type representing an optional value.
- Result - A type representing the result of some fallible computation.
Optional
A value of this type is either a Some
or a None
. Some
-values contain a value internally while None
-values represent the absence of a given value. This is useful when you have a value that may or may not exist. Where you might otherwise use null
or undefined
to represent the absense of a value, you can use a None
value instead.
Consider getting a property of an object that may or may not exist:
Normally you might do this:
const data = getSomeData(); // we may not know what data looks like here
let c = 0; // make sure you default it
if (typeof data.c !== 'undefined' && data.c !== null) { // make sure data.c exists
c = data.c;
}
doSomething(c); // now we can use c
Instead you could do this:
const data = getSomeData();
let optC = getProperty(data, 'c'); // returns an Optional
let c = optC.unwrapOr(0); // we can use unwrapOr to safely get the value or
// a default value if c wasn't present on data
doSomething(c);
Or if we want to avoid doing anything when c
doesn't exist:
const data = getSomeData();
let optC = getProperty(data, 'c'); // returns an Optional
optC.forEach(doSomething); // forEach will call doSomething with optC's internal
// value if it exists, otherwise nothing happens
The key to this being useful is that both Some
and None
are wrapped in the same API. This means you can call forEach
and unwrapOr
(and a bunch of other methods) on the return value regardless of whether it's a Some
or a None
.
Note: This library doesn't provide a getProperty()
function but one could imagine it looking something like:
function getProperty(obj, propName) {
if (typeof obj[propName] !== 'undefined' && obj[propName] !== null) {
return Optional.some(obj[propName]); // return a `Some`-value containing the value internally
}
return Optional.none(); // otherwise return a `None`-value
}
Result
A value of this type is either an Ok
or an Err
. Both of these contain an internal value, but they each convey a different meaning. Ok
is used to represent an operation that succeeded and returned some kind of successful result. Err
is used to represent an operation that failed and returned some kind of error.
Consider parsing a number out of a string:
Normally you might do this:
const aNumber = getANumber(); // we may not know if this is a valid number
const result = parseInt(aNumber, 10); // do our parsing
let parsed = 0; // default it
if (!Number.isNaN(result)) { // if it didn't fail, hold on to the parsed value
parsed = result;
}
doSomething(parsed); // now we can use it
Instead you could do this:
const aNumber = getANumber(); // we may not know if this is a valid number
const result = safeParseInt(aNumber, 10); // returns a Result
const parsed = result.unwrapOr(0); // we can use unwrapOr to safely get the value or
// a default value if the result was an Err-value
doSomething(parsed); // now we can use it
Or if you want to handle both cases explicitly:
const aNumber = getANumber(); // we may not know if this is a valid number
const result = safeParseInt(aNumber, 10); // returns a Result
// result.match will call the given ok function if the result is an Ok-value
// and it will call the given err function if it is an Err-value
result.match({
ok: value => { doSomething(value); }, // pass the parsed value to `doSomething`
err: error => { console.error(error); }, // or do whatever you need to do with the error
});
Again, the key here is that both Ok
and Err
values are wrapped in the same API. This means you can treat them both the same and just describe how you want them to behave instead of writing all of the boiler-plate logic every time you deal with them.
Note: This library doesn't provide a safeParseInt()
function but it might look something like this if it were provided:
function safeParseInt(num, radix) {
let parsed = parseInt(num, radix);
if (Number.isNaN(parsed)) {
// return an Err-value with a meaningful error message
return Result.Err(`Could not parse the value: ${num} as an integer with radix: ${radix}`);
}
// otherwise return an Ok-value with the parsed value inside
return Result.Ok(parsed);
}