Data (Either)
The previous examples of chaining used two implementations of FP
: Literal
and Empty
.
These two classes provide an implementation of Either
,
seen in Haskell,
and JavaScript libraries such as
fp-ts
,
Folktale,
and more.
Implementations of Either
provide a clear and unmistakable difference between the presence and absence of data -
avoiding null, undefined
,
and other troublesome ways of representing a lack of data.
Properties
Before getting too far, it may be best to look at the actual properties of Literal
and Empty
-
the type system will save you from the properties on the backend,
but it may be helpful to see how FP works, and how the objects look after the compiler strips
out type information.
The following definitions include properties defined in inherited classes to provide
more information that exist in the actual class definitions of |
Literal
class Literal<T> extends Data<T> {
kind: "Literal";
status: 200 | 201;
hasData: true;
hasError: false;
noValue: false;
data: T;
}
Empty
class Empty<T = any> extends NoValue<T> {
kind: "Empty";
status: 404;
hasData: false;
hasError: false;
noValue: true;
}
Using Literal/Empty Values
There are (at least) five different ways to evaluate and process values that are returned
as Literal
or Either
instances, with reasons why each may be good for certain situations.
We will be using the same data for all of the following examples, which is a basic example of loading a configuration value.
Literal
/Empty
for use in following examplesimport { readFileSync } from "fs";
function loadPortConfig(filename: string = "port.txt"): Literal<number> | Empty<number> {
try {
const fileContent = readFileSync(filename, "utf8");
const num = parseInt(fileContent, 10);
if(num && !isNaN(num)) {
return new Literal(num);
}
return new Empty();
} catch (e) {
return new Empty();
}
}
const portConfig = loadPortConfig();
Checking 'kind'
Every FP
object is required to include a kind
field,
which can determine if the returned config is a Literal
.
kind
var port: number;
if(portConfig.kind === "Literal") {
console.log(`Listening on port ${portConfig.data}, as-per config.`);
port = portConfig.data;
} else {
console.log("No configured port. Defaulting to '80'.");
port = 80;
}
Using Description properties (hasData
, etc.)
While kind
provides an exact match, every FP
also contains three flags
providing a more general description of an object:
hasData
, hasError
, noValue
.
This is overkill when only Either
or Literal
could be returned,
but you could use these flags to filter out errors and empty values:
hasError
, noValue
if(portConfig.hasError) {
console.log("Error loading port!");
} else if(portConfig.noValue) {
console.log("No port specified, will use default.");
} else {
console.log(`Port configured as ${portConfig.data}`);
}
Using status
All FP
objects include a status
code, intended to represent HTTP response status codes
(or process exit codes).
One could use these fields similar to the descriptor fields - e.g. responses with status
in the range 300
- 500
are likely errors, and this could be compared similar to the last example using hasError
- although we don’t see why anyone would prefer this.
However, if you are writing general middleware that formats FP
objects into HTTP responses,
you may find status
useful.
Utility Methods
The above examples used FP
properties, but every FP
object also includes utility
methods.
The first example used kind
to set a default if Empty
was found,
however altValue()
could avoid the entire if
/else
block.
altValue
const port = portConfig.altValue(80);