Understanding the types

fp-ts-std and its documentation make use of some type aliases from fp-ts, as well as the idea of newtypes. This document is a brief overview of these types.

Type aliases

As a refresher, type aliases hold no special value in the type system. They are merely placeholders that exist for convenience and/or to express semantic intent. For example, we could define a type alias representing email addresses like this:

type Email = string;

It can be used interchangeably absolutely anywhere with string. This is in contrast to newtypes which are discussed later on below.

Predicate

From fp-ts/Predicate::Predicate, the Predicate type alias expresses a very common use case - a function that takes a value and returns a boolean having performed a check on said value.

type Predicate<A> = (a: A) => boolean;

Refinement

From fp-ts/Refinement::Refinement, the Refinement type alias is very similar to Predicate, but with one key difference - the returned boolean also acts as a type guard on the function’s input. If you’re unfamiliar with type guards, check out the TypeScript handbook.

type Refinement<A, B> = (a: A) => a is B;

Here’s a trivial example of a type guard/refinement function:

const isString = (x: unknown): x is string => typeof x === 'string';

Which could be rewritten slightly to utilise this type alias, marginally improving readability at a glance:

const isString: Refinement<unknown, string> = (x): x is string => typeof x === 'string';

Endomorphism

From fp-ts/Endomorphism::Endomorphism, the Endomorphism type alias isn’t nearly as scary as it sounds. Here’s the type:

type Endomorphism<A> = (a: A) => A;

It’s just a function that takes and returns a value of the same type! If you’re curious about the name, it comes from mathematics and category theory.

Newtypes

fp-ts-std leverages newtypes via newtype-ts. Newtypes are like type aliases except they have a distinct, exclusive representation in the type system.

Here’s how we might represent our Email type alias above instead as a newtype:

import { Newtype } from 'newtype-ts';

type Email = Newtype<{ readonly Email: unique symbol }, string>;

Crucially, as mentioned, this Email type has a distinct representation in the type system, meaning that as far as the compiler is concerned Email and string are not interchangeable:

import { iso } from 'newtype-ts';

declare const f: Endomorphism<Email>;

const str = 'example@domain.tld';
const email = iso<Email>().wrap(str);

f(str) // error! string is not Email
f(email) // ok

You may find it useful to research the idea of “smart constructors” as they pertain to newtypes. That enables us to push certain logical bugs almost entirely into the type system.