excoptional
v1.3.7-beta
Published
A typed, standalone Option object for JavaScript and TypeScript
Downloads
3
Readme
Excoptional
A fully typed, zero-dependency implementation of the functional programming Option object for JavaScript and TypeScript.
Options are incredibly useful for functions that may or may not return a value and for providing a mechanism to safely manipulate that value regardless of whether it does or does not exist. Options help avoid tedious and repetitive checks to determine if a value is null
or undefined
before manipulating it.
This library is simple and provides the best possible TypeScript support for autocomplete, type hints and more.
Code Coverage is 100% across statements, branches, functions, and lines.
This package adheres to semantic versioning.
Benefits of Options (and this specific pacakge)
All the code examples below are runnable in a TypeScript environment with this package installed.
Without options, null
and undefined
checks are often necessary before manipulating a function's argument (even in TS codebases).
const uppercaseStrOne = (value: string): string => {
if (value !== null && value !== undefined) {
return value.toUpperCase();
}
return undefined;
}
const myInputOne = "hello world";
// parsedInput could be undefined (or whatever the author of uppercaseStrOne
// function decides when the condition is false).
const parsedInput = uppercaseStrOne(myInputOne);
With Options, no null
and undefined
checks are needed before manipulating the value.
import { Some, Option } from "excoptional";
const uppercaseStrTwo = (value: Option<string>): Option<string> => {
return value.map(str => str.toUpperCase());
}
const myInputTwo = Some("hello world");
const parsedString = uppercaseStrTwo(myInputTwo).getOrElse("whatever we want");
// => "HELLO WORLD" | "whatever we want"
- The argument and return type of the function changes from a
string
to anOption<string>
explicitly indicating that this function may or may not have a return value. - The caller of
uppercaseStrTwo
is forced to eventually handle both scenarios, but can continue to safely manipulate the underlying value viamap
and other available methods. - The caller avoids additional
null
andundefined
checks of the returned value. - The caller, instead of the function author, determines the fallback value if the Option does not contain a value (via the
getOrElse
method).
This package provides a mechanism to seamlessly transform functions like uppercaseStrOne
into uppercaseStrTwo
; That is, functions that work on non Option
values to functions that do work on Option
values without rewriting all your functions. This way, you can reuse as much of your existing code as possible.
// Using the original function uppercaseStrOne and the static version of map
// from this package, we create a upper case function that works
// on Option<string>.
const optionUpperCaseStr = Option.map(uppercaseStrOne);
// Invoke the new function passing in an Option as the argument
const parsedVal = optionUpperCaseStr(myInputTwo); // => Some("HELLO WORLD"); | None;
Getting Started
Install the Module
npm install --save excoptional
Use the Module
Instances of an Option
can be created using the Some()
and None()
functions, or the static of
method off the Option
class. These can all be imported off the module.
import { Some, None, Option } from "excoptional";
const myFirstOption = Some(42);
const mySecondOption = None();
const myThridOption = Option.of("Hello World") // Equivalent to Some("Hello World");
const myFourthOption = Option.of() // Equivalent to None();
Some
takes one argument (the value) while None
takes no arguments. The value to Some
or Option.of
can be anything, a primitive, javascript object, etc.
Avoid using the
new
keyword to instantiate instances of anOption
. In fact, the constructor is intentially declared as private to help avoid this behavior.
import { Some, None, Option } from "excoptional";
const getIfValid = (val: string): Option<string> => {
if (val.length > 2) {
return Some(val);
} else {
return None();
}
}
const myOption = getIfValid("Hello World");
const myOtherOption = getIfValid("Hi");
myOption.map(val => val.toUpperCase()); // => Some("HELLO WORLD");
myOtherOption.map(val => val.toUpperCase()); // None();
// Instantiate an instance of an Option via the static of method
const yetAnotherOption = Option.of<string>(); // => None();
const ourFinalOption = Option.of("wow"); // => Some("wow");
// and use them as any other option
const result = yetAnotherOption
.flatMap(val => {
if (val.length > 2) {
return Some("success");
} else {
return None();
}
})
.orElse(ourFinalOption) //=> Some('wow');
Notes and Best Practices
Typing Functions that Return Options
If you prefer explicit types on functions, prefer using Option<T>
(replacing T with the type definition for the underlying value) over Some<T> | None
.
Specifying the return type is not required; excoptional
supports type inference.
// Bad ❌
const myFunc = (): Some<string> | None => {
return Math.random() * 100 > 50 ? Some("Success") : None();
};
// Good ✅
const myFunc = (): Option<string> => {
return Math.random() * 100 > 50 ? Some("Success") : None();
};
// Good ✅
const myFunc = () => {
return Math.random() * 100 > 50 ? Some("Success") : None();
};
Logging Options
When doing console.log(myOpt);
it's best to do console.log(myOpt.toString())
. This provides better output. JavaScript does not automatically invoke an object's toString
method by default.
For convenience, there is a shorter named equivalent .toStr
method.
For even more convenience, there is a .log
method which will invoke console.log
and the toString
method for you.
import { Some } from "excoptional";
const myOpt = Some("Hello World");
// The below are all equivalent
console.log(myOpt.toString());
console.log(myOpt.toStr());
myOpt.log();
map vs flatMap vs then
If unsure of which method to use to transform the underlying value, then
should always work and be suitable.
- Use
map
when the provided function does not return an Option. - Use
flatMap
when the provided function does return an Option. then
can be used regardless if the provided function returns an Option or not.
flatten
Method Behavior
Most Option implementations have the potential to
throw
whenflatten
is invoked. This implementation will notthrow
. The behavior for the method is described below.
When calling the flatten
method
- if the current
Option
is aNone
, aNone
will be returned. - if the current
Option
is aSome
but the underlying value is not anOption
, the instance is returned. - if and only if, the instance is a
Some
and the underlying value is anOption
, will the underlying value be returned.
In essence, this method is guaranteed to always return an Option
and never throw an error. In fact, this package should never throw an error.
If it does, please file an issue.
Types
- Never pass
undefined
when invokingSome()
.None()
should replace all instances ofundefined
along withnull
. Some<T>
andOption<T>
are equivalent (whereT
is neithernull
norundefined
).None
andOption<undefined>
are equivalent.
Methods
The methods availale for an Option
in this package are similar to those in many FP languages and packages. They are detailed below.
/**
* Returns true if the instance is a None. Returns false otherwise
*/
isNone(): boolean
/**
* Returns true if the instance is a Some. Returns false otherwise
*/
isSome(): boolean
/**
* An alias for isSome
* Returns true if the instance is a Some. Returns false otherwise
*/
exists(): boolean
/**
* An alias for isSome
* Returns true if the instance is a Some. Returns false otherwise
*/
nonEmpty(): boolean
/**
* Returns the underlying value if the instance is a Some. Returns a
* None otherwise
*/
get(): A | None
/**
* Returns the underlying value if it's a Some. Returns the provided
* argument otherwise.
*/
getOrElse<B>(otherVal: B): A | B
/**
* Returns the current instance if it's a Some. Returns the provided
* Option argument otherwise.
*
* @remarks Useful for chaining successive calls to return the first
* Some in the chain of orElses.
*
* @example
* ```
* const firstSuccessfulOptionValue = fnReturnsOption
* .orElse(fn2ReturnsOption())
* .orElse(fn3ReturnsOption())
* .orElse(fn4ReturnsOption())
* ```
*/
orElse<B>(otherOption: Option<B>): Option<A> | Option<B>
/**
* Transforms the underlying value if the instance is a Some by
* applying the provided function to the underlying value, returning
* the transformed value in an Option.
* Returns a None otherwise.
*
* @remarks Prefer this to `flatMap` when the provided function does
* not return an Option.
*/
map<B>(fn: (val: A) => B): Option<B>
/**
* A static version of map. Useful for lifting functions of type
* (val: A) => B to be a function of type
* (val: Option<A>) => Option<B>.
*
* A curried version of map. First accepts the transformation
* function, and returns a function that accepts the Option.
*
* @example
* ```
* const appendToString = (val: string) => val + "@gmail.com";
*
* // Options (possibly returned by other functions):
* const opt = Some("johnsmith");
* const otherOpt = None();
*
* // Create a version of appendToString that works on values that
* // are Options
* const appendToOptionString = Option.map(appendToString);
*
* const maybeAnEmailAddress = appendToOptionString(opt);
* // maybeAnEmailAddress => Some("[email protected]")
*
* const maybeAnEmailAddress2 = appendToOptionString(otherOpt);
* // maybeAnEmailAddress2 => None();
* ```
*/
static map<B, A>(fn: (val: A) => B): (opt: Option<A>) => Option<B>
/**
* An alias for Option.map. Perhaps a more accurate or descriptive
* name.
*
* Lifts a function of type (val: A) => B
* to be a function of type (val: Option<A>) => Option<B>.
*
* @example
* // Working with number
* const addFive = (val: number) => val + 5;
* const eight = addFive(3);
*
* // Working with Option<number>
* const addFiveToOption = Option.lift(addFive);
* const maybeEight = addFiveToOption(Some(3));
*/
static lift<B, A>(fn: (
val: A
) => B): (
opt: Option<A>
) => Option<B>
/**
* Like Option.lift but for functions with an arbitrary number of
* arguments instead of just one.
*
* Lifts a function, with an arbitrary number of arguments, where
* each argument is not an Option, to be a function that works on
* Option versions of those arguments.
*
* @remarks This function has very weak type support and strict
* requirements to work correctly. Use with caution.
* @remarks The provided function **must** be completely curried.
* @remarks If any of the provided Option arguments are a None, a
* None will be returned.
* @remarks Each argument in the provided curried function must have
* the same type as its corresponding Option type. See the 2nd
* example below.
* @remarks All of the Option arguments for the provided function
* must be passed when liftN is invoked.
*
* @example
* ```
* Option.liftN<number>(
* (a: number) => (b: number) => (c: number) => a + b + c,
* Some(18),
* Some(4),
* Some(6)
* ); // => Some(28)
*
* // Since the 2nd argument (b) is defined as an object with a
* // property age whose type is a number, the 2nd Option must be
* // an Option whose underlying value is an Object with a property
* // called age whose value is a number. This required relationship
* // is **not** enforced by the type system.
* Option.liftN<number>(
* (a: number) => (b: { age: number }) => a + b.age,
* Some(78),
* Some({ age: 22 })
* ); // => Some(100)
*/
static liftN<T>(
fn,
...args: [Option<unknown>, ...Option<unknown>[]]
): Option<T>
/**
* Applies the function, wrapped in the current Option instance, to
* the provided Option, returning the result.
*
* @remarks This should only be used when the instance contains a
* function of the type (val: A) => B.
* @remarks Useful when the function to apply to another Option is
* itself wrapped in an Option.
*
* @example
* ```
* const getFunctionToUse = (): Option<(val: number) => number> => {
* return Math.random() > .5 ?
* Some((val) => val + 2) :
* None();
* }
*
* // functionToUse is Option<(val: number) => number>
* const functionToUse = getFunctionToUse();
*
* const some8 = Some(8);
* const maybe10 = functionToUse.ap(some8); // => Some(10)
* ```
*/
ap<B, C>(opt: Option<B>): Option<C>
/**
* Equivalent to map but returns the underlying value instead of an
* Option. Returns one of alternativeVal (if provided) or undefined
* if the instance is a None.
*/
fold<B>(fn: (val: A) => B, alternativeVal?: B): B | undefined
/**
* Transforms and returns the underlying value if the instance is a
* Some by applying the provided function to the underlying value.
* Returns a None otherwise.
*
* @remarks Prefer this to `map` when the provided function returns
* an Option.
*
* @remarks If unsure of which method to use between `map`,
* `flatMap`, and `then`, `then` should always work.
*/
flatMap<B>(fn: (val: A) => Option<B>): Option<B>
/**
* A static version of flatMap. Useful for lifting functions of type
* (val: A) => Option<B> to be a function of type
* (val: Option<A>) => Option<B>
*
* A curried version of flatMap. First accepts the transformation
* function, then the Option.
*
* @example
* ```
* const appendIfValid = (val: string): Option<string> => {
* if (val.length > 2) {
* const newVal = val + "@gmail.com";
* return Some(newVal);
* } else {
* return None();
* }
* }
*
* // Options (possibly returned by other parts of your code base)
* const opt = Some("johnsmith");
* const otherOpt = None();
*
* // Create a version of appendIfValid that works on Option<string>
* const appendToOptionStrIfValid = Option.flatMap(appendIfValid);
*
* const maybeAnEmailAddress = appendToOptionStrIfValid(opt);
* // maybeAnEmailAddress => Some("[email protected]")
*
* const maybeAnEmailAddress2 = appendToOptionStrIfValid(otherOpt);
* // maybeAnEmailAddress2 => None()
*
* // This next line is equivalent to the above.
* const maybeAnEmailAddress3 = Option.flatMap(appendIfValid)(opt);
* ```
*/
static flatMap<B, A>(
fn: (val: A) => Option<B>
): (opt: Option<A>) => Option<B>
/**
* Usable in place of both map and flatMap.
* Accepts a function that returns either an Option or non Option
* value.
* Always returns an Option.
*
* Makes the Option class into a thenable.
*
* If the instance is a None, a None is returned.
* If the provided function returns an Option, the result of
* applying the function to the underlying value is returned.
* If the provided function returns a non Option, the result of
* applying the function to the underlying value is lifted into an
* Option and returned.
*
* @example
* ```
* const myOpt = Some(10);
*
* const maybeDouble = (val: number): Option<number> => {
* Math.random() > .5 ?
* Some(val * 2) :
* None();
* }
*
* const alwaysDouble = (val: number): number => val * 2;
*
* // function calls can be chained with .then regarless if the
* // functions passed to then return an Option or non Option.
* const maybeOptDoubledOrQuadrupled = myOpt.then(maybeDouble)
* .then(alwaysDouble);
* ```
*/
then<B>(fn: (val: A) => B | Option<B>): Option<B>
/**
* Flattens and returns a wrapped Option.
*
* If the instance is a None, a None is returned.
* If the underlying value is **not** an Option, the instance is
* returned.
* If the underlying value is an option, the underlying value is
* returned.
* In all cases, an Option is **always** returned (making the method
* safe for chaining).
*
* @remarks It's impossible to automatically and definitively infer
* the underlying value's type. If the caller knows the possible
* type(s) of the underlying value, the possible type(s) can be
* passed through the generic.
*
* @example
* ```
* // Assume myFunc is of the type () => number | string;
* const wrappedOpt = Some(myFunc());
*
* // wrappedOpt's underlying value is of the type number | string.
* // Pass that type through the generic for accurate typing.
* const flattenedOption = wrappedOpt.flatten<number | string>();
* ```
*/
flatten<B>(): Option<B>
/**
* Returns the instance if the underlying value passes the provided
* filter function. Returns a None otherwise.
*/
filter(filterFn: (val: A) => boolean): Option<A>
/**
* Returns the instance if the underlying value **fails** the
* provided filter function. Returns a None otherwise.
*/
filterNot(filterFn: (val: A) => boolean): Option<A>
/**
* Returns true if the instance's underlying value equals the
* provided argument. Returns false otherwise.
*
* @remarks Accepts an optional equality function for comparing two
* values for when the underlying value is not a primitive. By
* default this equality function is JavaScript's ===.
*/
contains(
val: A,
equalityFn?: (valOne: A, valTwo: A) => boolean
): boolean
/**
* Returns an Array with the underlying value when the instance is a
* Some. Returns an empty Array otherwise.
*/
toArray(): [A] | []
/**
* Returns a Set containing the underlying value when the instance
* is a Some. Returns an empty Set otherwise.
*/
toSet(): Set<A>
/**
* Returns a string representation of the Option.
* Useful for console logging an instance.
*
* @example
* ```
* console.log(Some(3)); // => "Some(3)"
* console.log(None()); // => "None"
* ```
*/
toString(): string
/**
* An alias for toString();
*/
toStr(): string
/**
* Logs the Option to the console invoking both console.log and
* toString for you.
*
* Accepts an optional function (customToString) as an argument.
* customToString is a function you implement that returns a string.
* The string returned by customToString will be used in place of
* the string returned by toString method.
* customToString will have access to the Option instance as well
* but should **not** mutate the instance in any way (by calling
* map, flatMap, then, filter, etc).
*
* @example
* ```
* Some(3).log(); // => "Some(3)"
* None().log(); // => "None"
*
* const customLogger = (opt: Option<number>): string => {
* return ~~~~~~~~~~~ " + opt.toStr() + " ~~~~~~~~~~";
* }
*
* Some(3).log(customLogger) // => "~~~~~~~~~~ Some(3) ~~~~~~~~~~"
* // Or defined inline and not even using the instance
* Some(3).log(() => "-- I AM HERE --"); // => "-- I AM HERE --"
* ```
*/
log(customToString?: (opt: this) => string): void
/**
* Returns the instance after logging it to the console.
*
* Convenient to see the value of the Option in a sequence of method
* calls for debugging without having to split up the method calls.
*
* Accepts an optional function (customToString) as an argument.
* customToString is a function you implement that returns a string.
* The string returned by customToString will be used in place of
* the string returned by toString method.
* customToString will have access to the Option instance as well
* but should **not** mutate the instance in any way (by calling
* map, flatMap, then, filter, etc).
*
* @example
* const customLogger = <T>(opt: Option<T>): string => {
* return "!!!!!!!! " + opt.toStr() + " !!!!!!!!";
* }
* Some(3)
* .map(val => val + 5)
* .logAndContinue() // => "Some(8)"
* .map(val => val + 2)
* .filter(val => val > 10)
* .logAndContinue(customLogger) // => "!!!!!!!! None !!!!!!!!"
* .getOrElse(-1);
* ```
*/
logAndContinue(customToString?: (opt: this) => string): Option<A>
/**
* Returns an instance of an Option using the value passed to it (if
* provided). Equivalent to using the Some() or None() functions.
*/
static of<A>(val?: A): Option<A>