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

About

Bulletproof type guards in TypeScript.

Topics

Resources

License

Stars

Watchers

Forks