safe-types
v4.27.0
Published
Type safe utils inspired from the Rust language for writing better JavaScript.
Downloads
1,233
Readme
Safe Types
Type safe utils inspired from the Rust language for writing better JavaScript. Written in typescript with support for flow definitions.
API Documentation
Follow the link for method references. Below is an explanation of why and how.
Purpose
This library started out as an experiment both to learn Rust concepts as well as to determine whether some of Rust's types can be mapped to TypeScript and improve the safety of TypeScript/JavaScript.
Side Note: It's my opinion that a library like this requires a 100% TypeScript environment to provide security around JS types. Without the TypeScript compiler and tooling, these primitives may make your data more opaque rather than provide insight and clarity into the many states application data can be in. Using an editor like vscode can provide some built in intellisense when running in JavaScript without TypeScript, but inference is limited.
Concepts
The two main exports of this library are implementations of the Maybe Monad and the Either Monad. I wrote this library without any formal learning in category theory, so the Rust standard library was essential to guiding my implementation of these Monad patterns. While these functional programming concepts are notoriously arcane, they are much easier to learn in practice than in theory.
Option Type (Maybe Monad)
Imagine if you could rewrite JavaScript and remove null
and undefined
types.
The dreaded error undefined is not a function
could be gone forever!. Sounds
great, right? But there are still many times when you need a way to represent
nothing. What happens if your function doesn't return anything? It turns out
that there is a pattern for doing with type safety.
The option type is a simple type that can exist in one of two states--having
some value, or having none (nothing). In a way, this is very similar
to having a value or having null
. However, an option carries the context of
the type it represent regardless of it being present. Instead of calling a
function expecting a string
or null
, you receive an option of a string
Option<string>
. That option may be Some<string>
, or it may be
None
. This is why it's known as the Maybe Monad.
So how does this work? We wrap the state of having a value in a box. In
JavaScript, this box is implemented with a plain old JavaScript object (POJO).
Let's imagine what a Some<string>
would look like for the string
'typescript'
:
type Some<T> = { value: T };
let someString: Some<string> = { value: "typescript" };
This is fine for a simple box to hold our value, but how would we represent
None
? What about an empty object?
type None = {};
let noneString: None = {};
Now we have a consistent object shape, so we need a way to distinguish the state
of the option type. We want to keep our option generic, so it can't be
implemented with any kind of value checking on the value
property of the
Some
object. Instead, we can use another property that will be common to both
states that we call a type discriminant.
type Some<T> = {
value: T;
state: "Some";
};
type None = {
state: "None";
};
This is enough for us to implement the option type! But keeping track of the option state is tedious, and the abstraction shouldn't allow us to know the internal state. That's where the library comes in.
Usage
Generally, usage of the option type starts by wrapping a value of which we know
the type, but we also know that it may return null
or undefined
. To do that,
we can use the Option.of
or Option.from
methods (one is just an alias).
function getAtIndex(index, list) {
return Option.of(list[index]);
}
let list = [1, 2, 3];
let firstElement = getAtIndex(0, list);
// Some<1>
let probablyNot = getAtIndex(10, list);
// None
Both Option.of
and its alias only create a Some
type if the value is not
null
or undefined
. From then on, you can make use of the methods available
on the Option
type to perform operations safely. Let's try another example
like getting the input value from an HTMLInputElement.
// document.querySelector returns an element or null
let maybeInput = Option.of(document.querySelector("input[name=email]"));
// If we are certain this exists, we can unwrap the value.
// It will throw an error if the option is a `none`
let input = maybeInput.unwrap();
// We can also have it throw a custom error message
input = maybeInput.expect("expected an input element with name=email");
// We can safely read a value if we provide a default
let value = maybeInput.map_or("", element => element.value);
// We can even do a chain of dependent actions without dealing with null!
let maybeSubmitBtn = maybeInput.and_then(element =>
Option.of(element.parent.querySelector("button[type=submit]"))
);
You can browse the set of methods known as combinators, but the most important
method is match
. The match
method accepts an object with methods for each
possible state of the option (Some
and None
) and must return the same type
for each possibility.
let luckyNumber = Option.of(list.find(num => num > 100)).match({
// Called with the value.
Some(num) {
return num;
},
None() {
return 0;
},
});
Convert Tasks to Observables
The Task object is similar to RxJS observables, and can be converted in just a few lines of code.
const fetchTask = new Task(async ({ Ok, Err }) => {
try {
const response = await fetch("https://example.test/some/url");
const data = await response.json();
return Ok(data);
} catch (error) {
return Err(error);
}
});
const fetchStream = new Observable(async subscriber => {
await fetchTask.fork({
Ok: data => subscriber.next(data),
Err: error => subscriber.error(error),
});
subscriber.complete();
});
It is important to note that while the Task model maps nicely to Observables, it is missing the mechanisms to cancel its work. For this reason, there is no teardown function returned from the subscribe function passed to the Observable's constructor.
The lack of cancellation gets at the key differences between Tasks and Observables.
- Tasks model a single chain of asynchronous work that may produce a value; Observables model a stream of asynchronous values/signals.
- Tasks do not cancel since they are more like lazy promises; Observables manage a resource lifecycle including their construction, completion, & destruction.
- Tasks model type-safe errors; Observables treat errors opaquely since they are caught and therefore cannot provide type guarantees.