Shulk
Introduction Get startedBasics
Tagged unions Pattern matchingMonads
Result Maybe LoadingAsync
Procedure ConcurrentlyOthers
WrappersThe Maybe monad for optional value handling
Why: the Billion Dollar Mistake
Of all languages, TypeScript is far from being the one with the worst null
implementation, even if it still is evaluated as an “object”.
You will never get a NullPointerException
, but it won’t always make your code safer, as you’re not forced to handle it explicitely in some situations.
The solution: Use the Maybe monad
The Maybe monad is a generic type (and an union under the hood) which can has 2 states: Some (with a value attached), and None (with no value attached).
Type definition
type Maybe<T> = { val: T; _state: "Some" } | { _state: "None" };
Constructors
Some
function Some<T>(val: T): Maybe<T>;
None
function None(): Maybe<never>;
Methods
map
The map
method allows you to map the value contained to another value.
function map<O>(fn: (val: T) => O): Maybe<O>;
flatMap
The flatMap
method allows you to map the value contained to another Maybe
.
function flatMap<O>(fn: (val: T) => Maybe<O>): Maybe<O>;
filter
The filter
method allows you to map the value contained to an empty Maybe
if a condition is not met.
function filter(condition: (val: T) => boolean): Maybe<T>;
filterType
The filterType
method allows you to map the value contained to an empty Maybe
if a condition is not met, while narrowing down its type.
function filterType<O extends T>(condition: (val: T) => val is O): Maybe<O>;
toResult
The toResult
method allows you the Maybe
to a Result
.
function toResult<E>(otherwise: () => E): Result<E, T>;
Pattern matching
Maybe is an union, which means you can handle it with match
.
import { Maybe, Some, None } from "shulk";
function divide(dividend: number, divisor: number): Maybe<number> {
if (divisor == 0) {
return None();
} else {
return Some(dividend / divisor);
}
}
match(divide(2, 2)).case({
None: () => console.log("Could not compute result"),
Some: ({ val }) => console.log("Result is ", val),
});
Examples
Let’s take a divide
function that will return no value when confronted to a division by 0:
import { Maybe, Some, None } from "shulk";
function divide(dividend: number, divisor: number): Maybe<number> {
if (divisor == 0) {
return None();
} else {
return Some(dividend / divisor);
}
}
// We can then handle our Result in a few different ways
// unwrap() is unsafe as it will throw if confronted to the None state, but can be useful for prototyping
divide(2, 2).unwrap(); // 1
divide(2, 0).unwrap(); // Uncaught Error: Maybe is None
// expect() throws a custom message when it encounters an error state
// Like unwrap(), you shoudn't use it in a production context
divide(2, 2).expect("Too bad!"); // 1
divide(2, 0).expect("Too bad!"); // Uncaught Too bad!
// unwrapOr() will return the provided default value when encountering a None state
// It is safe to use in a production context, as the program cannot crash
divide(2, 2).unwrapOr("Not a number"); // 1
divide(2, 0).unwrapOr("Not a number"); // "Not a number"
// map() takes a function as an argument and return its value wrapped in a Some state, or a None state
divide(2, 2)
.map((res) => res.toString())
.unwrap(); // "1"
divide(2, 0)
.map((res) => res.toString())
.unwrap(); // Uncaught Error: Maybe is None
// flatMap() takes a function that returns a Maybe as an argument, and return its value
divide(2, 2)
.flatMap((res) => Some(res.toString()))
.unwrap(); // "1"
divide(2, 0)
.flatMap((res) => Some(res.toString()))
.unwrap(); // Uncaught Error: Maybe is None
// toResult() maps the Maybe to a Result monad
divide(2, 2).toResult(() => "Cannot divide by 0"); // Result<string, number>