A type-safe alternative to standard user-defined type guards.
npm install refinements
yarn add refinements
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);
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:
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)
);
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
.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
getRefinement
fromfp-ts