Skip to content

What it is

io-ts provides runtime type validation built on fp-ts, representing decoding failures as an Either rather than by throwing.

Define codecs that both decode unknown input and encode back out. TypeOf extracts the static type, and decode returns an Either.

Installation

npm install io-ts fp-ts

Getting started

The smallest useful thing you can do with it, and what each part means.

Codecs and decoding
import * as t from 'io-ts';
import { isLeft } from 'fp-ts/Either';

const User = t.type({
  id: t.number,
  email: t.string,
  bio: t.union([t.string, t.undefined]),
});

type User = t.TypeOf<typeof User>;

const decoded = User.decode(input);

if (isLeft(decoded)) {
  return { errors: PathReporter.report(decoded) };
}
const user = decoded.right;   // typed and verified
decode never throws — failure is a left value. PathReporter turns the error tree into readable strings, which is otherwise quite raw.

Advanced usage

Where the library earns its place over a simpler alternative.

Branded types for validated values
interface PositiveBrand { readonly Positive: unique symbol }

const Positive = t.brand(
  t.number,
  (n): n is t.Branded<number, PositiveBrand> => n > 0,
  'Positive',
);

type Positive = t.TypeOf<typeof Positive>;

// A function taking Positive cannot be passed an arbitrary number,
// so the check cannot be forgotten downstream.
function withdraw(amount: Positive) { /* … */ }
Branding encodes a validated invariant into the type, so the check happens once at the boundary and the compiler enforces it everywhere after.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Errors are an unreadable nested structure
Format with PathReporter.report or a custom reporter; the raw tree is not meant for display.
Optional fields are rejected
t.type requires every key. Use t.partial for optional ones and t.intersection to combine.

Best practices

  • Prefer Zod for new projects unless the codebase is already fp-ts based.
  • Use PathReporter to turn error trees into readable messages.
  • Use branded types for values with invariants that must survive past validation.
  • Remember codecs encode as well as decode — useful for symmetric serialisation.

Background

Why it exists, and what it was reacting to.

Also by Giulio Canti, io-ts predates Zod and takes the functional route: a codec is a value, and decoding returns Either<Errors, A> so failure is handled compositionally.