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-tsGetting started
The smallest useful thing you can do with it, and what each part means.
typescript
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 verifiedAdvanced usage
Where the library earns its place over a simpler alternative.
typescript
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) { /* … */ }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.
