0.1.4 • Published 4 years ago

refinements v0.1.4

Weekly downloads
4
License
MIT
Repository
github
Last release
4 years ago

Installation

npm install refinements
yarn add refinements

Usage

import Refinement from 'refinements';

class Mango {}
class Orange {}

type Fruit = Mango | Orange;

const isMango: Refinement<Fruit, Mango> = Refinement.create(
  fruit =>
    fruit instanceof Mango
      ? Refinement.hit(fruit)
      : Refinement.miss
);

const fruits: Fruit[] = [new Mango(), new Orange()];
const mangos: Mango[] = fruits.filter(isMango);

Why?

By default, user-defined type guard are not type-checked. This leads to silly errors.

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

TypeScript is happy to accept such buggy code.

The create function exposed by this library is type-checked. Let's see how it helps create bulletproof type-guards by rewriting the original implementation.

import Refinement from 'refinements';

const isString: Refinement<unknown, string> = Refinement.create(
  candidate =>
    typeof candidate === 'string'
      ? Refinement.hit(candidate)
      : Refinement.miss
);

If we tried to replace, say, typeof candidate === 'string' with the incorrect typeof candidate === 'number', we would get a compile-time error.

Examples

Composition

Let's assume the following domain.

abstract class Fruit {
  readonly species: string;
}

class Orange extends Fruit {
  readonly species: 'orange';
}

class Mango extends Fruit {
  readonly species: 'mango';
}

abstract class Vegetable {
  nutritious: boolean;
}

type Merchendise = Fruit | Vegetable;

To navigate the hierarchy of our domain, we can create a refinement for every union that occurs in the domain.

import Refinement from 'refinements';

const isFruit: Refinement<Merchendise, Fruit> = Refinement.create(
  merchendise =>
    merchendise instanceof Fruit
      ? Refinement.hit(merchendise)
      : Refinement.miss
);

const isOrange: Refinement<Fruit, Orange> = Refinement.create(
  fruit =>
    fruit instanceof Orange
      ? Refinement.hit(fruit)
      : Refinement.miss
);

const isMango: Refinement<Fruit, Mango> = Refinement.create(
  fruit =>
    fruit instanceof Mango
      ? Refinement.hit(fruit)
      : Refinement.miss
);

Such refinements can be composed together.

import { either, compose } from 'refinements';

const isJuicy =
  compose(
    isFruit,
    either(isOrange, isMango)
);

Negation

import Refinement, { not } from 'refinements';

type Standard = 'inherit' | 'initial' | 'revert' | 'unset';
type Prefixed = '-moz-initial';

type Property = Standard | Prefixed;

// We can cherry-pick the one that stands out
const isPrefixed = Refinement.create(
  (property: Property) =>
    property === '-moz-initial'
      ? Refinement.hit(property)
      : Refinement.miss
);

// And get the rest by negating the first one
const isStandard = not(isPrefixed);

⚠️ Warning! This is an experimental feature. For this to work, the union members have to be mutually exclusive. If you do something like this:

declare function isString(candidate: any): candidate is string;

const isNotString = not(isString);

It will work, but the inferred type will be (candidate: any) => candidate is any.

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Inspiration

License

MIT

0.1.4

4 years ago

0.1.3

4 years ago

0.1.2

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago