TypeScript runtime type system for IO decoding/encoding
72
Advanced type construction for adding runtime constraints and creating branded types.
Add runtime constraints to existing codecs with custom predicate functions.
/**
* Add a refinement predicate to an existing codec
* @param codec - The base codec to refine
* @param predicate - Predicate function for additional validation
* @param name - Optional name for the refined codec
*/
function refinement<C extends Any, B extends TypeOf<C>>(
codec: C,
refinement: Refinement<TypeOf<C>, B>,
name?: string
): RefinementC<C, B>;
function refinement<C extends Any>(
codec: C,
predicate: Predicate<TypeOf<C>>,
name?: string
): RefinementC<C>;
interface RefinementC<C extends Any, B = TypeOf<C>> extends RefinementType<C, B, OutputOf<C>, InputOf<C>> {}
/** Predicate function type */
type Predicate<A> = (a: A) => boolean;
/** Refinement function type */
type Refinement<A, B extends A> = (a: A) => a is B;Usage Examples:
import * as t from "io-ts";
// Create a positive number codec
const PositiveNumber = t.refinement(
t.number,
(n): n is number => n > 0,
'PositiveNumber'
);
const result1 = PositiveNumber.decode(42);
// result: Right(42)
const result2 = PositiveNumber.decode(-5);
// result: Left([validation error])
// Create an email codec using string refinement
const Email = t.refinement(
t.string,
(s): s is string => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),
'Email'
);
const email = Email.decode("user@example.com");
// result: Right("user@example.com")
// Create non-empty array codec
const NonEmptyArray = <A>(codec: t.Type<A>) =>
t.refinement(
t.array(codec),
(arr): arr is [A, ...A[]] => arr.length > 0,
`NonEmptyArray<${codec.name}>`
);
const NonEmptyStrings = NonEmptyArray(t.string);
const result3 = NonEmptyStrings.decode(["hello"]);
// result: Right(["hello"])
const result4 = NonEmptyStrings.decode([]);
// result: Left([validation error])Create branded types that are distinct at the type level while sharing runtime representation.
/**
* Create a branded codec that adds a compile-time brand to values
* @param codec - The base codec to brand
* @param predicate - Refinement predicate for the brand
* @param name - Brand name (becomes part of the type)
*/
function brand<C extends Any, N extends string, B extends { readonly [K in N]: symbol }>(
codec: C,
predicate: Refinement<TypeOf<C>, Branded<TypeOf<C>, B>>,
name: N
): BrandC<C, B>;
interface BrandC<C extends Any, B> extends RefinementType<C, Branded<TypeOf<C>, B>, OutputOf<C>, InputOf<C>> {}
/** Brand interface for creating branded types */
interface Brand<B> {
readonly [_brand]: B;
}
/** Branded type combining base type with brand */
type Branded<A, B> = A & Brand<B>;Usage Examples:
import * as t from "io-ts";
// Create a UserId brand
interface UserIdBrand {
readonly UserId: unique symbol;
}
const UserId = t.brand(
t.number,
(n): n is t.Branded<number, UserIdBrand> => Number.isInteger(n) && n > 0,
'UserId'
);
type UserId = t.TypeOf<typeof UserId>;
// UserId = number & Brand<UserIdBrand>
const userId = UserId.decode(123);
// result: Right(123) with UserId brand
// Create an Email brand
interface EmailBrand {
readonly Email: unique symbol;
}
const EmailBranded = t.brand(
t.string,
(s): s is t.Branded<string, EmailBrand> => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),
'Email'
);
type Email = t.TypeOf<typeof EmailBranded>;
// Email = string & Brand<EmailBrand>
// Branded types prevent accidental mixing
function sendEmail(to: Email, from: UserId) {
// Implementation
}
const email = EmailBranded.decode("user@example.com");
const id = UserId.decode(456);
if (email._tag === "Right" && id._tag === "Right") {
sendEmail(email.right, id.right); // Type safe
// sendEmail(id.right, email.right); // Type error!
}Pre-defined integer codec using the branding system.
/** Integer brand interface */
interface IntBrand {
readonly Int: unique symbol;
}
/** Branded integer type */
type Int = Branded<number, IntBrand>;
/** Integer codec that validates integer numbers */
const Int: BrandC<NumberC, IntBrand>;
/** Pre-defined Integer codec (deprecated, use Int) */
const Integer: RefinementC<NumberC>;Usage Example:
import * as t from "io-ts";
const result1 = t.Int.decode(42);
// result: Right(42) with Int brand
const result2 = t.Int.decode(42.5);
// result: Left([validation error - not an integer])
const result3 = t.Int.decode("42");
// result: Left([validation error - not a number])
// Type-level distinction
function processInt(value: t.Int) {
// value is guaranteed to be an integer
return value * 2;
}
if (result1._tag === "Right") {
const doubled = processInt(result1.right); // Type safe
}import * as t from "io-ts";
// Chain multiple refinements
const PositiveEvenNumber = t.refinement(
t.refinement(
t.number,
(n): n is number => n > 0,
'PositiveNumber'
),
(n): n is number => n % 2 === 0,
'EvenNumber'
);
const result = PositiveEvenNumber.decode(10);
// result: Right(10) - positive and even
const invalid1 = PositiveEvenNumber.decode(-2);
// result: Left([validation error - not positive])
const invalid2 = PositiveEvenNumber.decode(3);
// result: Left([validation error - not even])import * as t from "io-ts";
// Create a URL brand
interface UrlBrand {
readonly Url: unique symbol;
}
const Url = t.brand(
t.string,
(s): s is t.Branded<string, UrlBrand> => {
try {
new URL(s);
return true;
} catch {
return false;
}
},
'Url'
);
type Url = t.TypeOf<typeof Url>;
// Create a non-empty string brand
interface NonEmptyStringBrand {
readonly NonEmptyString: unique symbol;
}
const NonEmptyString = t.brand(
t.string,
(s): s is t.Branded<string, NonEmptyStringBrand> => s.length > 0,
'NonEmptyString'
);
type NonEmptyString = t.TypeOf<typeof NonEmptyString>;
// Use branded types in complex structures
const UserProfile = t.type({
name: NonEmptyString,
website: Url,
id: t.Int
});
type UserProfile = t.TypeOf<typeof UserProfile>;import * as t from "io-ts";
// Create range validation
const range = (min: number, max: number) =>
t.refinement(
t.number,
(n): n is number => n >= min && n <= max,
`Range(${min}, ${max})`
);
const Age = range(0, 150);
const Percentage = range(0, 100);
const Temperature = range(-273.15, Infinity);
const User = t.type({
name: t.string,
age: Age,
score: Percentage
});
const result = User.decode({
name: "Alice",
age: 25,
score: 95
});
// result: Right({ name: "Alice", age: 25, score: 95 })
const invalid = User.decode({
name: "Bob",
age: 200, // Too old
score: 110 // Over 100%
});
// result: Left([validation errors for age and score])import * as t from "io-ts";
// Create pattern-based string codecs
const pattern = (regex: RegExp, name: string) =>
t.refinement(
t.string,
(s): s is string => regex.test(s),
name
);
const PhoneNumber = pattern(/^\+?[\d\s()-]+$/, 'PhoneNumber');
const PostalCode = pattern(/^\d{5}(-\d{4})?$/, 'PostalCode');
const HexColor = pattern(/^#[0-9A-Fa-f]{6}$/, 'HexColor');
const ContactInfo = t.type({
phone: PhoneNumber,
zip: PostalCode,
favoriteColor: HexColor
});
const result = ContactInfo.decode({
phone: "+1 (555) 123-4567",
zip: "12345",
favoriteColor: "#FF0000"
});
// result: Right(contact info)import * as t from "io-ts";
// Create a sophisticated ID system
interface ProductIdBrand {
readonly ProductId: unique symbol;
}
interface CategoryIdBrand {
readonly CategoryId: unique symbol;
}
const ProductId = t.brand(
t.refinement(
t.string,
(s): s is string => s.startsWith('PROD-') && s.length === 10,
'ProductIdFormat'
),
(s): s is t.Branded<string, ProductIdBrand> => true,
'ProductId'
);
const CategoryId = t.brand(
t.refinement(
t.string,
(s): s is string => s.startsWith('CAT-') && s.length === 9,
'CategoryIdFormat'
),
(s): s is t.Branded<string, CategoryIdBrand> => true,
'CategoryId'
);
type ProductId = t.TypeOf<typeof ProductId>;
type CategoryId = t.TypeOf<typeof CategoryId>;
// These types are now completely distinct
function assignCategory(productId: ProductId, categoryId: CategoryId) {
// Implementation
}
const prodId = ProductId.decode("PROD-12345");
const catId = CategoryId.decode("CAT-54321");
if (prodId._tag === "Right" && catId._tag === "Right") {
assignCategory(prodId.right, catId.right); // Type safe
// assignCategory(catId.right, prodId.right); // Type error!
}Install with Tessl CLI
npx tessl i tessl/npm-io-tsdocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10