Runtime validation for static types
—
Every runtype provides essential validation methods for different use cases. These methods form the core API for runtime type checking and offer different approaches depending on whether you need exceptions, type guards, or detailed results.
Every runtype implements these fundamental validation methods.
/**
* Core validation methods available on every runtype
*/
interface Runtype<T> {
/**
* Validates and returns the value, throwing ValidationError on failure
* @param x - Value to validate
* @returns Validated value with proper typing
* @throws ValidationError when validation fails
*/
check<U>(x: U): T & U;
/**
* Type guard that returns boolean indicating validation success
* @param x - Value to validate
* @returns True if valid, false otherwise (no exceptions)
*/
guard<U>(x: U): x is T & U;
/**
* Assertion function that throws ValidationError on failure
* @param x - Value to validate
* @throws ValidationError when validation fails
*/
assert<U>(x: U): asserts x is T & U;
/**
* Validates and parses the value, applying transformations
* @param x - Value to validate and parse
* @returns Parsed/transformed value
* @throws ValidationError when validation fails
*/
parse<U>(x: U): T;
/**
* Detailed validation with structured result (no exceptions)
* @param x - Value to validate
* @param options - Validation options
* @returns Result object with success/failure details
*/
inspect<U>(x: U, options?: { parse?: boolean }): Result<T>;
}The most commonly used validation method. Returns the validated value or throws an exception.
import { String, Number, Object, Array } from "runtypes";
// Basic usage
const name = String.check("Alice"); // "Alice"
const age = Number.check(25); // 25
// Throws ValidationError on failure
try {
const invalid = String.check(123);
} catch (error) {
console.log("Validation failed:", error.message);
}
// Type narrowing with intersection
function processUserData(input: unknown) {
const userData = Object({
name: String,
age: Number,
email: String
}).check(input);
// userData is now properly typed
console.log(userData.name.toUpperCase()); // TypeScript knows name is string
console.log(userData.age + 10); // TypeScript knows age is number
}
// Array validation
const scores = Array(Number).check([95, 87, 92]); // number[]
scores.forEach(score => console.log(score * 1.1)); // TypeScript knows each score is numberReturns a boolean and acts as a TypeScript type guard. Never throws exceptions.
import { String, Number, Object, Union, Literal } from "runtypes";
// Basic type guarding
function processValue(input: unknown) {
if (String.guard(input)) {
// TypeScript knows input is string here
return input.toUpperCase();
}
if (Number.guard(input)) {
// TypeScript knows input is number here
return input * 2;
}
return "Unknown type";
}
// Object type guarding
const User = Object({
id: Number,
name: String,
active: Boolean
});
function handleUserData(data: unknown) {
if (User.guard(data)) {
// data is now typed as { id: number; name: string; active: boolean }
console.log(`User ${data.id}: ${data.name} (${data.active ? 'active' : 'inactive'})`);
} else {
console.log("Invalid user data");
}
}
// Union type discrimination
const Status = Union(Literal("pending"), Literal("approved"), Literal("rejected"));
function handleStatus(status: unknown) {
if (Status.guard(status)) {
// status is now typed as "pending" | "approved" | "rejected"
switch (status) {
case "pending":
console.log("Waiting for approval");
break;
case "approved":
console.log("Request approved");
break;
case "rejected":
console.log("Request rejected");
break;
}
}
}Assertion function that throws on failure but doesn't return a value. Useful for type narrowing.
import { String, Number, Object } from "runtypes";
// Basic assertion
function processString(input: unknown) {
String.assert(input);
// input is now typed as string
return input.toLowerCase();
}
// Multiple assertions
function processUserInput(data: unknown) {
const UserData = Object({
name: String,
age: Number,
email: String
});
UserData.assert(data);
// data is now properly typed
console.log(`Processing user: ${data.name}`);
console.log(`Age: ${data.age}`);
console.log(`Email: ${data.email}`);
}
// Conditional assertions
function validateConfig(config: unknown) {
if (typeof config === "object" && config !== null) {
Object({
apiKey: String,
timeout: Number,
retries: Number.optional()
}).assert(config);
// config is now properly typed
return config;
}
throw new Error("Config must be an object");
}Validates and applies parsing/transformation. The difference from check is that parse applies any parsers in the runtype chain.
import { String, Number, Parser, Object } from "runtypes";
// Basic parsing (same as check for simple types)
const name = String.parse("Alice"); // "Alice"
const age = Number.parse(25); // 25
// With parser transformations
const UppercaseString = String.withParser(s => s.toUpperCase());
const result = UppercaseString.parse("hello"); // "HELLO"
const DateFromString = String.withParser(s => new Date(s));
const date = DateFromString.parse("2024-01-15"); // Date object
// Complex object parsing
const UserInput = Object({
name: String,
age: String, // Input as string
preferences: String // JSON string
});
const UserProcessor = UserInput.withParser(input => ({
name: input.name.trim(),
age: parseInt(input.age, 10),
preferences: JSON.parse(input.preferences),
processedAt: new Date()
}));
const user = UserProcessor.parse({
name: " Alice ",
age: "25",
preferences: '{"theme": "dark", "notifications": true}'
});
// {
// name: "Alice",
// age: 25,
// preferences: { theme: "dark", notifications: true },
// processedAt: Date
// }
// Chained transformations
const ProcessedString = String
.withParser(s => s.trim())
.withParser(s => s.toLowerCase())
.withParser(s => s.replace(/\s+/g, "-"));
const slug = ProcessedString.parse(" Hello World "); // "hello-world"Returns detailed Result object without throwing exceptions. Useful when you need comprehensive error information.
import { String, Number, Object, Array, type Result } from "runtypes";
// Basic inspection
const stringResult = String.inspect("hello");
if (stringResult.success) {
console.log("Valid string:", stringResult.value);
} else {
console.log("Error:", stringResult.message);
console.log("Code:", stringResult.code);
}
// Parsing vs checking with inspect
const UppercaseString = String.withParser(s => s.toUpperCase());
// Without parsing
const checkResult = UppercaseString.inspect("hello", { parse: false });
// Returns original value if valid
// With parsing (default)
const parseResult = UppercaseString.inspect("hello", { parse: true });
// Returns transformed value if valid
// Complex validation with detailed errors
const User = Object({
profile: Object({
name: String,
age: Number
}),
settings: Object({
theme: Union(Literal("light"), Literal("dark")),
notifications: Boolean
})
});
function analyzeValidation(data: unknown): void {
const result = User.inspect(data);
if (result.success) {
console.log("✓ Validation successful");
return;
}
console.log("✗ Validation failed:");
console.log(`Main error: ${result.code} - ${result.message}`);
if (result.details) {
console.log("Detailed errors:");
analyzeDetails(result.details, "");
}
}
function analyzeDetails(details: Record<string, any>, path: string) {
for (const [key, detail] of Object.entries(details)) {
const currentPath = path ? `${path}.${key}` : key;
if (!detail.success) {
console.log(` ${currentPath}: ${detail.code} - ${detail.message}`);
if (detail.details) {
analyzeDetails(detail.details, currentPath);
}
}
}
}
// Usage
analyzeValidation({
profile: {
name: "Alice",
age: "not a number" // Error
},
settings: {
theme: "invalid", // Error
notifications: true
}
});Beyond validation, every runtype provides methods for composition, constraints, and transformations.
Combine runtypes using logical operations.
/**
* Composition methods for combining runtypes
*/
interface Runtype<T> {
/**
* Create union type (logical OR)
* @param other - Another runtype to union with
* @returns Union runtype accepting either type
*/
or<R extends Runtype.Core>(other: R): Union<[this, R]>;
/**
* Create intersection type (logical AND)
* @param other - Another runtype to intersect with
* @returns Intersect runtype requiring both types
*/
and<R extends Runtype.Core>(other: R): Intersect<[this, R]>;
}Usage Examples:
import { String, Number, Object } from "runtypes";
// Union using .or()
const StringOrNumber = String.or(Number);
StringOrNumber.check("hello"); // "hello"
StringOrNumber.check(42); // 42
StringOrNumber.check(true); // throws ValidationError
// Intersection using .and()
const PersonBase = Object({ name: String });
const PersonWithAge = Object({ age: Number });
const CompletePerson = PersonBase.and(PersonWithAge);
CompletePerson.check({ name: "Alice", age: 30 }); // ✓
CompletePerson.check({ name: "Bob" }); // ✗ missing ageShortcuts for common nullable/optional patterns.
/**
* Methods for optional and nullable types
*/
interface Runtype<T> {
/** Make property optional (can be absent) */
optional(): Optional<this, never>;
/** Make property optional with default value */
default<V = never>(value: V): Optional<this, V>;
/** Allow null values */
nullable(): Union<[this, Literal<null>]>;
/** Allow undefined values */
undefinedable(): Union<[this, Literal<undefined>]>;
/** Allow null or undefined values */
nullishable(): Union<[this, Literal<null>, Literal<undefined>]>;
}Usage Examples:
import { String, Number, Object } from "runtypes";
// Optional properties in objects
const User = Object({
name: String,
email: String,
age: Number.optional(), // Can be absent
bio: String.default("No bio provided"), // Default value if absent
avatar: String.nullable(), // Can be null
lastSeen: String.undefinedable(), // Can be undefined
metadata: String.nullishable() // Can be null or undefined
});
type UserType = Static<typeof User>;
// {
// name: string;
// email: string;
// age?: number;
// bio: string;
// avatar: string | null;
// lastSeen: string | undefined;
// metadata: string | null | undefined;
// }Add custom validation logic while preserving types.
/**
* Methods for adding constraints and custom validation
*/
interface Runtype<T> {
/**
* Add constraint with boolean or error message return
* @param constraint - Function returning boolean or error string
* @returns Constrained runtype
*/
withConstraint<Y extends T>(constraint: (x: T) => boolean | string): Constraint<this, Y>;
/**
* Add type guard constraint
* @param guard - TypeScript type guard function
* @returns Constrained runtype with narrowed type
*/
withGuard<Y extends T>(guard: (x: T) => x is Y): Constraint<this, Y>;
/**
* Add assertion constraint
* @param assert - TypeScript assertion function
* @returns Constrained runtype with narrowed type
*/
withAssertion<Y extends T>(assert: (x: T) => asserts x is Y): Constraint<this, Y>;
}Usage Examples:
import { String, Number } from "runtypes";
// Constraint with boolean
const PositiveNumber = Number.withConstraint(n => n > 0);
// Constraint with error message
const NonEmptyString = String.withConstraint(
s => s.length > 0 || "String cannot be empty"
);
// Email validation with regex
const Email = String.withConstraint(
s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) || "Invalid email format"
);
// Type guard constraint
const isPositive = (n: number): n is number => n > 0;
const PositiveWithGuard = Number.withGuard(isPositive);
// Assertion constraint
const assertIsEmail = (s: string): asserts s is string => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) {
throw new Error("Invalid email");
}
};
const EmailWithAssertion = String.withAssertion(assertIsEmail);Add semantic meaning and transformations to types.
/**
* Methods for branding and parsing/transformation
*/
interface Runtype<T, X = T> {
/**
* Add brand to create nominal typing
* @param brand - Brand identifier string
* @returns Branded runtype
*/
withBrand<B extends string>(brand: B): Brand<B, this>;
/**
* Add transformation/parsing logic
* @param parser - Function to transform validated value
* @returns Parser runtype with transformed output type
*/
withParser<Y>(parser: (value: X) => Y): Parser<this, Y>;
}Usage Examples:
import { String, Number, type Static } from "runtypes";
// Branded types for domain modeling
const UserId = String.withBrand("UserId");
const ProductId = String.withBrand("ProductId");
type UserIdType = Static<typeof UserId>; // string & Brand<"UserId">
type ProductIdType = Static<typeof ProductId>; // string & Brand<"ProductId">
// These are now different types despite both being strings
const userId = UserId.check("user-123");
const productId = ProductId.check("prod-456");
// Type error: can't assign UserId to ProductId variable
// const wrongId: ProductIdType = userId; // ✗
// Parsing transformations
const DateFromString = String.withParser(s => new Date(s));
const UppercaseString = String.withParser(s => s.toUpperCase());
const NumberFromString = String.withParser(s => parseInt(s, 10));
// Chaining parsers
const PositiveIntFromString = String
.withParser(s => parseInt(s, 10))
.withConstraint(n => n > 0 || "Must be positive");
const result = PositiveIntFromString.parse("42"); // number (42)Add custom properties and utilities to runtypes.
/**
* Utility methods for extending and introspecting runtypes
*/
interface Runtype<T, X = T> {
/**
* Add custom properties to the runtype
* @param extension - Object or function returning properties to add
* @returns Extended runtype with additional properties
*/
with<P extends object>(extension: P | ((self: this) => P)): this & P;
/**
* Create a shallow clone of the runtype
* @returns Cloned runtype
*/
clone(): this;
/**
* Ensure runtype conforms to a specific TypeScript type
* @returns Runtype with type conformance checking
*/
conform<V, Y = V>(this: Conform<V, Y>): Conform<V, Y> & this;
}
/**
* Static methods on the Runtype constructor
*/
interface RuntypeStatic {
/**
* Check if a value is a runtype
* @param x - Value to check
* @returns Type guard for runtype
*/
isRuntype(x: unknown): x is Runtype.Interfaces;
/**
* Assert that a value is a runtype
* @param x - Value to assert
* @throws If value is not a runtype
*/
assertIsRuntype(x: unknown): asserts x is Runtype.Interfaces;
}Usage Examples:
import { String, Number, Runtype } from "runtypes";
// Adding custom utilities to runtypes
const Username = String
.withConstraint(s => s.length >= 3 || "Username too short")
.with({
defaultValue: "anonymous",
validateUnique: async (username: string) => {
// Custom async validation logic
return !await userExists(username);
}
});
// Access custom properties
console.log(Username.defaultValue); // "anonymous"
await Username.validateUnique("newuser");
// Using function for extension (access to self)
const Temperature = Number.with(self => ({
celsius: (fahrenheit: number) => {
const celsius = (fahrenheit - 32) * 5/9;
return self.check(celsius);
},
fahrenheit: (celsius: number) => {
const fahrenheit = celsius * 9/5 + 32;
return self.check(fahrenheit);
}
}));
const temp = Temperature.celsius(100); // 37.77...
// Type conformance
interface User {
name: string;
age: number;
email?: string;
}
const UserRuntype = Object({
name: String,
age: Number,
email: String.optional()
}).conform<User>(); // Ensures exact match with User interface
// Static utility methods
const maybeRuntype: unknown = String;
if (Runtype.isRuntype(maybeRuntype)) {
// maybeRuntype is now typed as Runtype.Interfaces
const result = maybeRuntype.check("test");
}
// Assert for type narrowing
Runtype.assertIsRuntype(maybeRuntype); // Throws if not runtype
// maybeRuntype is now typed as Runtype.Interfacesimport { String, Number, Object } from "runtypes";
const UserSchema = Object({
id: Number,
name: String,
email: String
});
// check() - Most common, when you want the validated value
function processUser(data: unknown) {
try {
const user = UserSchema.check(data);
return `User: ${user.name} (${user.email})`;
} catch (error) {
return "Invalid user data";
}
}
// guard() - Type narrowing in conditionals
function maybeProcessUser(data: unknown) {
if (UserSchema.guard(data)) {
// data is now typed, no exception handling needed
return `User: ${data.name} (${data.email})`;
}
return "Not a user";
}
// assert() - When you need type narrowing but not the return value
function validateAndProcess(data: unknown) {
UserSchema.assert(data); // Throws if invalid
// data is now typed as user object
// Continue processing with typed data
processUserEmail(data.email);
updateUserName(data.name);
}
// parse() - When transformations are involved
const UserWithTransform = UserSchema.withParser(user => ({
...user,
displayName: `${user.name} <${user.email}>`,
processedAt: new Date()
}));
function createDisplayUser(data: unknown) {
return UserWithTransform.parse(data); // Returns transformed object
}
// inspect() - When you need detailed error analysis
function validateWithReport(data: unknown) {
const result = UserSchema.inspect(data);
return {
isValid: result.success,
data: result.success ? result.value : null,
errors: result.success ? [] : collectErrors(result)
};
}import { String, Object } from "runtypes";
const SimpleSchema = Object({ name: String, age: Number });
// guard() is fastest for boolean checks (no exception creation)
function fastCheck(data: unknown): boolean {
return SimpleSchema.guard(data);
}
// check() has exception overhead on failure
function checkWithTryCatch(data: unknown) {
try {
return SimpleSchema.check(data);
} catch {
return null;
}
}
// inspect() provides detailed results without exceptions
function detailedCheck(data: unknown) {
const result = SimpleSchema.inspect(data);
return result.success ? result.value : null;
}
// For hot paths where you expect mostly valid data, use check()
// For validation where failures are common, use guard() or inspect()
// For debugging and development, use inspect() for detailed feedbackimport { ValidationError } from "runtypes";
// Pattern 1: Try-catch with check
function safeProcess(data: unknown) {
try {
const validated = UserSchema.check(data);
return { success: true, data: validated };
} catch (error) {
if (ValidationError.isValidationError(error)) {
return { success: false, error: error.message };
}
throw error; // Re-throw non-validation errors
}
}
// Pattern 2: Guard with fallback
function guardProcess(data: unknown) {
if (UserSchema.guard(data)) {
return processValidUser(data);
}
return getDefaultUser();
}
// Pattern 3: Inspect with detailed handling
function inspectProcess(data: unknown) {
const result = UserSchema.inspect(data);
if (result.success) {
return result.value;
}
// Handle specific error types
switch (result.code) {
case "TYPE_INCORRECT":
return handleTypeError(result);
case "PROPERTY_MISSING":
return handleMissingProperty(result);
case "CONTENT_INCORRECT":
return handleContentError(result);
default:
return handleGenericError(result);
}
}Install with Tessl CLI
npx tessl i tessl/npm-runtypes