Overview
Elegant and battle-tested validation library for type-safe input data for TypeScript.
Example
You write a decoder that describes the shape of data you expect. It looks almost identical to the TypeScript type it produces:
import { array, iso8601, number, object, optional, string } from 'decoders';
const userDecoder = object({
id: number,
name: string,
createdAt: optional(iso8601),
tags: array(string),
});Then call .verify() on untrusted input to validate it and get full type inference:
const user = userDecoder.verify(externalData);
// ^^^^
// TypeScript will infer this type as:
// {
// id: number;
// name: string;
// createdAt?: Date;
// tags: string[];
// }If verification fails, you get a clear error message pointing at the problem:
Decoding error:
{
id: 123,
name: "Alison Roberts",
createdAt: "not-a-date",
^^^^^^^^^^^^ Must be Date
tags: ["foo", "bar"],
}Why decoders?
Decoders is one of the OG runtime validation libraries for TypeScript, in production since 2017. Here's what makes it different:
- Tiny footprint — ~4 KB gzipped, zero external dependencies, fully tree-shakeable. You only pay for what you use.
- Runs everywhere — Node.js, browsers, Cloudflare Workers, Bun, etc.
- Reads like a type definition — No
z.string()orz.object(). You writeobject({ name: string, age: number })and it looks almost identical to the TypeScript type it produces. - Best-in-class error messages — When validation fails, decoders echoes back your actual input data with annotations inlined exactly where and why validation failed. Optimized for human readability.
- Standard Schema compliant — Works out of the box with any framework that supports Standard Schema.
Getting started
npm install decodersYou must set strict: true in your tsconfig.json for type inference to work correctly:
{
"compilerOptions": {
"strict": true
}
}How it works
The central building block is the Decoder. A Decoder<T> takes
untrusted input and either accepts it (returning a value of type T) or rejects it with a
helpful error.
Small decoders compose together like LEGO to build larger ones. In the example above,
string, number, and iso8601 are all individual decoders that get combined with
object, array, and optional.

Using a decoder
Call one of these methods on any decoder:
| Method | Returns | On failure |
|---|---|---|
.verify() | T | Throws an error |
.value() | T | undefined | Returns undefined |
.decode() | Result<T> | Returns error result |
Most of the time, .verify() is what you want.
Composing decoders
Every decoder in this library can be used standalone or composed with others. You can also build your own to match any shape of data.
// Compose decoders to describe complex shapes
const addressDecoder = object({
street: string,
city: string,
zip: regex(/^\d{5}$/, 'Must be 5-digit zip code'),
});
const userDecoder = object({
name: nonEmptyString,
email: email,
address: addressDecoder,
roles: array(oneOf(['admin', 'editor', 'viewer'])),
});Error formatting
By default, .verify() uses formatInline, which echoes back the input with errors
inlined:
import { array, object, string, number } from 'decoders';
const decoder = array(object({ name: string, age: number }));
decoder.verify([{ name: 'Alice', age: '33' }]);
// Decoding error:
// [
// {
// name: "Alice",
// age: "33",
// ^^^^ Must be number
// },
// ]You can also use formatShort for a compact single-line format:
import { formatShort } from 'decoders';
decoder.verify([{ name: 'Alice', age: '33' }], formatShort);
// Decoding error: Value at keypath '0.age': Must be number