Formulaic Docs

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 and Empty.

Properties of Literal
class Literal<T> extends Data<T> {
  kind: "Literal";

  status: 200 | 201;

  hasData: true;
  hasError: false;
  noValue: false;

  data: T;
}
Properties of 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.

Example Literal/Empty for use in following examples
import { 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.

Example using 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:

Example filtering by 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.

Example with altValue
const port = portConfig.altValue(80);

Using instanceof

This method is least recommended, but in some environments, instanceof will work to determine the class hierarchy used to create the FP object.

if(portConfig instanceof Literal) {
  console.log("Port was configured!");
} else {
  console.log("No port configured, using defaults.");
}