Skip to content

karol-majewski/refinements

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Refinements

A type-safe alternative to standard user-defined type guards.

Minified + gzipped size PRs welcome Latest Release

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.

Learn more about how it works:

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 Merchandise = 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<Merchandise, Fruit> = Refinement.create(
  merchandise =>
    merchandise instanceof Fruit
      ? Refinement.hit(merchandise)
      : 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