Option

The kind of Option is * -> *. That is, like Array, it can accept a Concrete Type A and produce another Concrete Type Option A. We can think of Option A as a box that may or may not contain a value of type A. Option is defined in FP-TS like this:

type Option<A> = None | Some<A>;
Copy the code

In TypeScript, we usually use the undefined or null type to indicate that a value may not exist. Such as:

type CurrentUser = User | null;
Copy the code

So what are the advantages of using Option

to represent the same semantics as defined above? Suppose we now implement a function that passes in the current user as an argument and returns the user’s name if the user exists; Otherwise, return undefined. The User type is defined as follows:

interface User {
  name: string;
  age: number; image? : {url: string;
  };
}
Copy the code

If null is used to indicate a missing value, then we need to use an if statement to determine if currentUser exists, and the code would look something like this:

function foo(currentUser: CurrentUser) {
  if (currentUser) {
    return currentUser.name;
  }
  return undefined;
}
Copy the code

The code for the Option

version is as follows:

import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/function";

function foo(currentUser: Option<User>) {
  return pipe(
    currentUser,
    O.map((user) = > user.name),
    O.toUndefined
  );
}
Copy the code

Where the map function is Functor, defined by typeclass, just as Eq defines equals. The map function types are defined as follows:

const map: <A, B>(f: (a: A) => B) => (fa: Option<A>) => Option<B>
Copy the code

That is, map takes a function of type A -> b and returns a function of type Option A -> Option B. This operation is called lift. The type Option A -> Option B means it knows how to handle cases where the value does not exist. Let’s look at the map function implementation:

const map: <A, B>(f: (a: A) => B) => (fa: Option<A>) => Option<B> = (f) => (fa) =>
  isNone(fa) ? none : some(f(fa.value))
Copy the code

It turns out that the map function surreptitiously checks the existence of values for us. In the above code, O.map((user) => user.name) returns a function of type Option user -> Option String. After accepting currentUser, the function returns a value of type Option String indicating that the user name may exist. The O.t oUndefined is Option < string > types into the ordinary string | is undefined. Array also has Functor instances, and array.map can be seen as an implementation of the map function used as array.

If the requirements now change, we return the user’s image link address (image may not exist). At this point we need to judge the conditions increased:

function foo(currentUser: CurrentUser) {
  if (currentUser && currentUser.image) {
    return currentUser.image.url
  }
  return undefined;
}
Copy the code

In optional chaining operator (? .). Before the advent of a complex object to write a property like

if (foo && foo.bar && foo.bar.baz) {
  // ...
}
Copy the code

Such code is very common. Does Option help us avoid such code? We continue to use the map function to help us avoid writing if statements:

interface User {
  name: string;
  age: number;
  image: Option<{
    url: string; } >. }function foo(currentUser: Option<User>) {
  return pipe(
    currentUser,
    map(u= > pipe(u.image, map(i= > i.url))),
    // ...
  );
}
Copy the code

This doesn’t look too good because u.mage is still an Option, so we need to use a nested map to convert u.mage to a value of type Option String. This nesting of maps is not only cumbersome, but also results in the return type being Option (Option String). As objects become more complex, options will become more layered like nesting dolls, which will result in more and more map nesting layers to use. This is certainly not a very ideal situation. We want to be able to change an Option (Option String) value to an Option String value. Implement a function called flatten:

const flatten = <A>(mma: Option<Option<A>>): Option<A> =>
  isNone(mma) ? none : mma.value;
Copy the code

With flatten, we can continue with this function:

function foo(currentUser: Option<User>) {
  return pipe(
    currentUser,
    map((u) = >
      pipe(
        u.image,
        map((i) = > i.url)
      )
    ),
    flatten
  );
}
Copy the code

In fact, FP-TS already provides the flatten function, but its implementation is a little different:

const flatten: <A>(mma: Option<Option<A>>) = > Option<A> = chain(identity)
Copy the code

Why is there another chain function? The chain function is defined by typeclass Monad and is implemented as follows:

const chain: <A, B>(f: (a: A) => Option<B>) => (ma: Option<A>) => Option<B> = (f) => (ma) =>
  isNone(ma) ? none : f(ma.value)
Copy the code

When you plug in identify, you get exactly the same result as flatten, which means that the two methods are equivalent.

After observation, the types of chain and map are similar. If we pass a function of type a -> Option B to map, we get a function of type Option A -> Option (Option B). If we pass the same function to chain, It will return us a function of type Option A -> Option B, as if flatten the result for us automatically. So, we try to replace map and flatten directly with chain:

function foo(currentUser: Option<User>) {
  return pipe(
    currentUser,
    chain((u) = >
      pipe(
        u.image,
        map((i) = > i.url)
      )
    ),
  );
}
Copy the code

The result is exactly the same as the above version.

Option also has other Typeclass instances, to be added later. Let’s look at an example of fP-TS using Option:

The type of findFirst function provided by FP-ts /Array to find Array elements is defined as follows:

interface Predicate<A> {
  (a: A): boolean
}

function findFirst<A> (predicate: Predicate<A>) : (as: Array<A>) = >Option<A>
Copy the code

Based on the provided predicate, findFirst finds elements in the array that satisfy the conditions, and returns Some if they exist. Otherwise, None is returned. Option is often used to indicate that the result of a calculation may succeed or fail, but it does not contain the details of the failure.

Either

In functional programming, exceptions are not handled with A throw exception. Instead, Either E A is returned to indicate that the result may be wrong. In contrast to Etiher, try-catch is not type-safe, and the compiler does not know what error a try statement will throw. And Either has more information about failure than Option does. The definition of Either in FP-TS is as follows:

Either<E, A> = Left<E> | Right<A>
Copy the code

The kind of Either is * -> * -> *, which can accept up to two types. Unfortunately, to be a member of the Functor club, the kind of type must be * -> *. This also means that Either does not support our definition of Functor instance for it, but luckily Either e’s kind is * -> *. So we define the map function for Either e:

const map: <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B> = (f) => (fa) =>
  isLeft(fa) ? fa : right(f(fa.right))
Copy the code

Either E means that the first variable of Either is no longer free, and neither map nor chain can replace the first variable with anything other than e. For example, map is used to turn lift, a function of type A -> b, into Either e a -> Either e b. Looking at the map implementation, you can see that it treats Left just as the Map of Options treats None. In addition, Either E has a lot in common with Option, so I won’t expand too much here. Take a look at an example of using Either, and then move on to the next type:

function stringify(. args: Parameters<typeof JSON.stringify>
) :Either<Error.string> {
  try {
    const stringified = JSON.stringify(... args);if (typeofstringified ! = ='string') {
      throw new Error('Converting unsupported structure to JSON')}return right(stringified);
  } catch (error) {
    returnleft(error); }}Copy the code

Developers may not know that json.stringify can throw an Error unless they consult the documentation, and it’s obvious that stringify that uses Either Error String as its return value might fail.