Runtime validation for TypeScript

Funtypes is a lightweight library for building runtime validators and parsers for TypeScript types with the best possible static type inference.

types.ts
package.json
import * as ft from "funtypes"
export const UserSchema = ft.Object({
id: ft.Number,
name: ft.String,
email: ft.String,
})
export type User = ft.Static<typeof UserSchema>
// => { id: number; name: string; email: string }
assert.deepEqual(
UserSchema.parse({
id: 42,
name: "Forbes Lindesay",
email: "forbes@example.com",
}),
{
id: 42,
name: "Forbes Lindesay",
email: "forbes@example.com",
},
)

Introduction

Getting started

Funtypes allow you to take values about which you have no assurances and check that they conform to some type A. It also lets you parse serialized values into rich objects. This is done by means of composable type validators of primitives, literals, arrays, tuples, records, unions, intersections and more.

Installation

npm install --save funtypes

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 Codecs in a similar structure to the TypeScript types:

import * as ft from 'funtypes';

const Vector = ft.Tuple(ft.Number, ft.Number, ft.Number);

const Asteroid = ft.Object({
  type: ft.Literal('asteroid'),
  location: Vector,
  mass: ft.Number,
});

const Planet = ft.Object({
  type: ft.Literal('planet'),
  location: Vector,
  mass: ft.Number,
  population: ft.Number,
  habitable: ft.Boolean,
});

const Rank = ft.Union(
  ft.Literal('captain'),
  ft.Literal('first mate'),
  ft.Literal('officer'),
  ft.Literal('ensign'),
);

const CrewMember = ft.Object({
  name: ft.String,
  age: ft.Number,
  rank: Rank,
  home: Planet,
});

const Ship = ft.Object({
  type: ft.Literal('ship'),
  location: Vector,
  mass: ft.Number,
  name: ft.String,
  crew: ft.Array(CrewMember),
});

const SpaceObject = ft.Union(Asteroid, Planet, Ship);

Now if we are given a SpaceObject from an untrusted source, we can validate it like so:

// spaceObject: SpaceObject
const spaceObject = SpaceObject.parse(obj);

If the object doesn't conform to the type specification, parse will throw an exception.

Static type inference

In TypeScript, the inferred type of Asteroid in the above example is

ft.Codec<{
  type: 'asteroid'
  location: [number, number, number]
  mass: number
}>

That is, it's a Codec<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 Codec-derived type like so:

import * as ft from 'funtypes';

type Asteroid = ft.Static<typeof Asteroid>;

which achieves the same result as

type Asteroid = {
  type: 'asteroid';
  location: [number, number, number];
  mass: number;
};
Next
Codec