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.