Runtypes allow you to take values about which you have no assurances and check that they conform to some type A
.
This is done by means of composable type validators of primitives, literals, arrays, tuples, records, unions,
intersections and more.
npm install --save runtypes
Suppose you have objects which represent asteroids, planets, ships and crew members. In TypeScript, you might write their types like so:
type Vector = [number, number, number];
type Asteroid = {
type: 'asteroid';
location: Vector;
mass: number;
};
type Planet = {
type: 'planet';
location: Vector;
mass: number;
population: number;
habitable: boolean;
};
type Rank = 'captain' | 'first mate' | 'officer' | 'ensign';
type CrewMember = {
name: string;
age: number;
rank: Rank;
home: Planet;
};
type Ship = {
type: 'ship';
location: Vector;
mass: number;
name: string;
crew: CrewMember[];
};
type SpaceObject = Asteroid | Planet | Ship;
If the objects which are supposed to have these shapes are loaded from some external source, perhaps a JSON file, we need to
validate that the objects conform to their specifications. We do so by building corresponding Runtype
s in a very straightforward
manner:
import { Boolean, Number, String, Literal, Array, Tuple, Record, Union } from 'runtypes';
const Vector = Tuple(Number, Number, Number);
const Asteroid = Record({
type: Literal('asteroid'),
location: Vector,
mass: Number,
});
const Planet = Record({
type: Literal('planet'),
location: Vector,
mass: Number,
population: Number,
habitable: Boolean,
});
const Rank = Union(
Literal('captain'),
Literal('first mate'),
Literal('officer'),
Literal('ensign'),
);
const CrewMember = Record({
name: String,
age: Number,
rank: Rank,
home: Planet,
});
const Ship = Record({
type: Literal('ship'),
location: Vector,
mass: Number,
name: String,
crew: Array(CrewMember),
});
const SpaceObject = Union(Asteroid, Planet, Ship);
(See the examples directory for an expanded version of this.)
Now if we are given a putative SpaceObject
we can validate it like so:
// spaceObject: SpaceObject
const spaceObject = SpaceObject.check(obj);
If the object doesn't conform to the type specification, check
will throw an exception.
In TypeScript, the inferred type of Asteroid
in the above example is
Runtype<{
type: 'asteroid'
coordinates: [number, number, number]
mass: number
}>
That is, it's a Runtype<Asteroid>
, and you could annotate it as such. But we don't really have to define the
Asteroid
type in TypeScript at all now, because the inferred type is correct. Defining each of your types
twice, once at the type level and then again at the value level, is a pain and not very DRY.
Fortunately you can define a static Asteroid
type which is an alias to the Runtype
-derived type like so:
import { Static } from 'runtypes';
type Asteroid = Static<typeof Asteroid>;
which achieves the same result as
type Asteroid = {
type: 'asteroid';
coordinates: [number, number, number];
mass: number;
};
In addition to providing a check
method, runtypes can be used as type guards:
function disembark(obj: {}) {
if (SpaceObject.guard(obj)) {
// obj: SpaceObject
if (obj.type === 'ship') {
// obj: Ship
obj.crew = [];
}
}
}
The Union
runtype offers the ability to do type-safe, exhaustive case analysis across its variants using the match
method:
const isHabitable = SpaceObject.match(
asteroid => false,
planet => planet.habitable,
ship => true,
);
if (isHabitable(spaceObject)) {
// ...
}
There's also a top-level match
function which allows testing an ad-hoc sequence of runtypes:
const makeANumber = match(
[Number, n => n * 3],
[Boolean, b => b ? 1 : 0],
[String, s => s.length],
);
makeANumber(9); // = 27
To allow the function to be applied to anything and then handle match failures, simply use an Always
case at the end:
const makeANumber = match(
[Number, n => n * 3],
[Boolean, b => b ? 1 : 0],
[String, s => s.length],
[Always, () => 42]
);
Beyond mere type checking, we can add arbitrary runtime constraints to a Runtype
:
const Positive = Number.withConstraint(n => n > 0);
Positive.check(-3); // Throws error: Failed constraint check
You can provide more descriptive error messages for failed constraints by returning
a string instead of false
:
const Positive = Number.withConstraint(n => n > 0 || `${n} is not positive`);
Positive.check(-3); // Throws error: -3 is not positive
Runtypes along with constraint checking are a natural fit for enforcing function
contracts. You can construct a contract from Runtype
s for the parameters and
return type of the function:
const divide = Contract(
// Parameters:
Number,
Number.withConstraint(n => n !== 0 || 'division by zero'),
// Return type:
Number,
).enforce((n, m) => n / m);
divide(10, 2); // 5
divide(10, 0); // Throws error: division by zero
- runtypes-generate Generates random data by
Runtype
for property-based testing