Runtime validation for static types
—
Pattern matching and helper utilities for working with runtypes. These tools provide additional functionality for advanced validation scenarios and convenient type manipulation.
Pattern matching utilities for working with union types and discriminated unions.
/**
* Creates a pattern matcher for union types
* @param cases - Array of case handlers created with when()
* @returns Function that matches value against cases
* @example match(when(String, s => s.length), when(Number, n => n * 2))
*/
function match<C extends readonly Case[]>(...cases: C): Matcher<C>;
/**
* Creates a case for pattern matching
* @param runtype - Runtype to match against
* @param transformer - Function to execute when matched
* @returns Case object for use with match()
* @example when(String, s => s.toUpperCase())
*/
function when<T, U>(runtype: Runtype<T>, transformer: (value: T) => U): Case<T, U>;
type Matcher<C> = (value: unknown) => ReturnType<C[number]>;
type Case<T, U> = [runtype: Runtype<T>, transformer: (value: T) => U];Usage Examples:
import { match, when, String, Number, Boolean, Object, Union, Literal } from "runtypes";
// Basic pattern matching
const processValue = match(
when(String, str => `String: ${str.toUpperCase()}`),
when(Number, num => `Number: ${num * 2}`),
when(Boolean, bool => `Boolean: ${bool ? "yes" : "no"}`)
);
console.log(processValue("hello")); // "String: HELLO"
console.log(processValue(42)); // "Number: 84"
console.log(processValue(true)); // "Boolean: yes"
// Discriminated union pattern matching
const Shape = Union(
Object({ type: Literal("circle"), radius: Number }),
Object({ type: Literal("rectangle"), width: Number, height: Number }),
Object({ type: Literal("triangle"), base: Number, height: Number })
);
const calculateArea = match(
when(Object({ type: Literal("circle"), radius: Number }),
({ radius }) => Math.PI * radius ** 2),
when(Object({ type: Literal("rectangle"), width: Number, height: Number }),
({ width, height }) => width * height),
when(Object({ type: Literal("triangle"), base: Number, height: Number }),
({ base, height }) => (base * height) / 2)
);
const area = calculateArea({ type: "circle", radius: 5 }); // ~78.54import { match, when, Object, Literal, String, Number, Array } from "runtypes";
// API response types
const SuccessResponse = Object({
status: Literal("success"),
data: Unknown
});
const ErrorResponse = Object({
status: Literal("error"),
message: String,
code: Number
});
const LoadingResponse = Object({
status: Literal("loading"),
progress: Number.optional()
});
// Pattern matcher for responses
const handleApiResponse = match(
when(SuccessResponse, ({ data }) => ({
type: "success" as const,
payload: data
})),
when(ErrorResponse, ({ message, code }) => ({
type: "error" as const,
error: `${code}: ${message}`
})),
when(LoadingResponse, ({ progress = 0 }) => ({
type: "loading" as const,
progress
}))
);
// Usage
const response1 = handleApiResponse({ status: "success", data: { users: [] } });
// { type: "success", payload: { users: [] } }
const response2 = handleApiResponse({ status: "error", message: "Not found", code: 404 });
// { type: "error", error: "404: Not found" }import { match, when, Union, Object, String, Number, Array } from "runtypes";
// Complex data processing
const DataProcessor = match(
// Process strings
when(String, str => ({
type: "text",
length: str.length,
words: str.split(" ").length,
uppercase: str.toUpperCase()
})),
// Process numbers
when(Number, num => ({
type: "numeric",
value: num,
squared: num ** 2,
isEven: num % 2 === 0
})),
// Process arrays
when(Array(Unknown), arr => ({
type: "list",
count: arr.length,
isEmpty: arr.length === 0,
first: arr[0],
last: arr[arr.length - 1]
})),
// Process objects
when(Object({ name: String, age: Number }), person => ({
type: "person",
greeting: `Hello, ${person.name}!`,
isAdult: person.age >= 18,
category: person.age < 13 ? "child" : person.age < 20 ? "teen" : "adult"
}))
);
// Usage examples
console.log(DataProcessor("Hello World"));
// { type: "text", length: 11, words: 2, uppercase: "HELLO WORLD" }
console.log(DataProcessor(42));
// { type: "numeric", value: 42, squared: 1764, isEven: true }
console.log(DataProcessor([1, 2, 3]));
// { type: "list", count: 3, isEmpty: false, first: 1, last: 3 }
console.log(DataProcessor({ name: "Alice", age: 25 }));
// { type: "person", greeting: "Hello, Alice!", isAdult: true, category: "adult" }Union types also provide their own pattern matching method.
import { Union, Object, Literal, String, Number } from "runtypes";
const Result = Union(
Object({ type: Literal("ok"), value: String }),
Object({ type: Literal("error"), message: String })
);
// Using Union's built-in match method
const processResult = Result.match(
// Case for "ok" variant
({ value }) => `Success: ${value}`,
// Case for "error" variant
({ message }) => `Failed: ${message}`
);
const result1 = processResult({ type: "ok", value: "Hello" }); // "Success: Hello"
const result2 = processResult({ type: "error", message: "Oops" }); // "Failed: Oops"While Optional is primarily used in object definitions, it can be useful for creating optional validation chains.
import { Optional, String, Number } from "runtypes";
// Create optional validators
const OptionalString = Optional(String);
const OptionalNumber = Optional(Number, 0); // with default
// Check for optional presence
function processOptionalValue(value: unknown) {
if (Optional.isOptional(OptionalString)) {
console.log("This is an optional type");
}
// Optional validation allows undefined
const result = OptionalString.underlying.guard(value);
return result;
}For recursive type definitions and forward references.
import { Lazy, Object, String, Number, Array, Union } from "runtypes";
// Recursive data structures
type TreeNodeType = {
value: string;
children: TreeNodeType[];
};
const TreeNode: Runtype<TreeNodeType> = Lazy(() => Object({
value: String,
children: Array(TreeNode) // Self-reference
}));
// Usage
const tree = TreeNode.check({
value: "root",
children: [
{
value: "child1",
children: [
{ value: "grandchild1", children: [] },
{ value: "grandchild2", children: [] }
]
},
{ value: "child2", children: [] }
]
});
// Mutually recursive types
type PersonType = {
name: string;
company?: CompanyType;
};
type CompanyType = {
name: string;
employees: PersonType[];
};
const Person: Runtype<PersonType> = Lazy(() => Object({
name: String,
company: Company.optional()
}));
const Company: Runtype<CompanyType> = Lazy(() => Object({
name: String,
employees: Array(Person)
}));Validate instanceof relationships with constructor functions.
import { InstanceOf, Object, String, Union } from "runtypes";
// Built-in constructors
const DateValidator = InstanceOf(Date);
const RegExpValidator = InstanceOf(RegExp);
const ErrorValidator = InstanceOf(Error);
const date = DateValidator.check(new Date()); // Date
const regex = RegExpValidator.check(/pattern/); // RegExp
// Custom classes
class User {
constructor(public name: string, public age: number) {}
}
class Admin extends User {
constructor(name: string, age: number, public permissions: string[]) {
super(name, age);
}
}
const UserValidator = InstanceOf(User);
const AdminValidator = InstanceOf(Admin);
const user = new User("Alice", 25);
const admin = new Admin("Bob", 30, ["read", "write"]);
UserValidator.check(user); // ✓ User instance
UserValidator.check(admin); // ✓ Admin extends User
AdminValidator.check(admin); // ✓ Admin instance
AdminValidator.check(user); // ✗ throws ValidationError
// Combined with other validators
const UserData = Union(
InstanceOf(User),
Object({ name: String, age: Number }) // Plain object alternative
);Basic function validation (note: cannot validate function signatures at runtime).
import { Function as FunctionValidator } from "runtypes";
// Validate that something is a function
const fn = FunctionValidator.check(() => "hello"); // Function
const method = FunctionValidator.check(Math.max); // Function
// Combined with other types
const Callback = Union(FunctionValidator, Literal(null));
function processWithCallback(data: unknown, callback: unknown) {
const validCallback = Callback.check(callback);
if (typeof validCallback === "function") {
return validCallback(data);
}
return data; // No callback provided
}Enable rest/spread syntax in tuple definitions.
import { Spread, Tuple, Array, String, Number } from "runtypes";
// Tuple with rest elements
const LogEntry = Tuple(
String, // timestamp
String, // level
Spread(Array(String)) // variable message parts
);
const entry1 = LogEntry.check(["2024-01-15T10:30:00Z", "INFO", "User", "logged", "in"]);
const entry2 = LogEntry.check(["2024-01-15T10:31:00Z", "ERROR", "Database", "connection", "failed", "retry", "in", "5s"]);
// Mixed tuple with leading, rest, and trailing
const DataRecord = Tuple(
Number, // id
String, // name
Spread(Array(Number)), // scores (variable length)
Boolean // active
);
const record = DataRecord.check([1, "Alice", 95, 87, 92, 88, true]);
// id=1, name="Alice", scores=[95,87,92,88], active=trueimport { match, when, Union, Object, Literal, String, Number, Array } from "runtypes";
// Complex validation and processing pipeline
const DataInput = Union(
String,
Number,
Array(String),
Object({ type: Literal("user"), name: String, age: Number }),
Object({ type: Literal("product"), name: String, price: Number })
);
const processInput = match(
when(String, str => ({ kind: "text", processed: str.trim().toLowerCase() })),
when(Number, num => ({ kind: "numeric", processed: num.toString() })),
when(Array(String), arr => ({ kind: "list", processed: arr.join(", ") })),
when(Object({ type: Literal("user"), name: String, age: Number }),
user => ({ kind: "user", processed: `${user.name} (${user.age})` })),
when(Object({ type: Literal("product"), name: String, price: Number }),
product => ({ kind: "product", processed: `${product.name}: $${product.price}` }))
);
// Usage
const results = [
processInput(" Hello World "),
processInput(42),
processInput(["apple", "banana", "cherry"]),
processInput({ type: "user", name: "Alice", age: 25 }),
processInput({ type: "product", name: "Widget", price: 19.99 })
];
results.forEach(result => {
console.log(`${result.kind}: ${result.processed}`);
});
// text: hello world
// numeric: 42
// list: apple, banana, cherry
// user: Alice (25)
// product: Widget: $19.99Install with Tessl CLI
npx tessl i tessl/npm-runtypes