/runtypes

Runtime validation for static types

Primary LanguageTypeScriptMIT LicenseMIT

Runtypes Build Status

Safely bring untyped data into the fold

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.

Installation

npm install --save runtypes

Example

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 Runtypes 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)

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.

Static type inference

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
}

Type guards

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 = []
        }
    }
}

Constraint checking

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

Function contracts

Runtypes along with constraint checking are a natural fit for enforcing function contracts. You can construct a contract from Runtypes 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