Runtime validation for static types
—
Combines multiple runtypes to create flexible validation schemas. Union types validate values that match any of several alternatives, while intersection types require values to match all specified types.
Validates values that match at least one of the provided alternative runtypes.
/**
* Creates a validator that accepts values matching any of the provided alternatives
* @param alternatives - Array of runtypes, value must match at least one
* @example Union(String, Number).check("hello") // "hello"
* @example Union(String, Number).check(42) // 42
* @example Union(String, Number).check(true) // throws ValidationError
*/
function Union<T extends readonly Runtype[]>(...alternatives: T): UnionRuntype<T>;
interface UnionRuntype<T> extends Runtype<UnionType<T>> {
tag: "union";
alternatives: T;
match<C extends Cases<T>>(...cases: C): Matcher<T, ReturnType<C[number]>>;
}Usage Examples:
import { Union, String, Number, Boolean, Literal, Object } from "runtypes";
// Basic union types
const StringOrNumber = Union(String, Number);
const value1 = StringOrNumber.check("hello"); // "hello"
const value2 = StringOrNumber.check(42); // 42
// Multiple alternatives
const ID = Union(String, Number, BigInt);
const userId = ID.check("user_123"); // "user_123"
const postId = ID.check(456); // 456
// Literal unions (enum-like)
const Status = Union(
Literal("pending"),
Literal("approved"),
Literal("rejected"),
Literal("cancelled")
);
type StatusType = Static<typeof Status>; // "pending" | "approved" | "rejected" | "cancelled"
// Nullable types using union
const OptionalString = Union(String, Literal(null));
const OptionalNumber = Union(Number, Literal(undefined));
// Or use built-in helpers
const NullableString = String.nullable(); // Union(String, Literal(null))
const UndefinedableNumber = Number.undefinedable(); // Union(Number, Literal(undefined))Create type-safe discriminated unions for complex data structures.
import { Union, Object, Literal, String, Number, Array } from "runtypes";
// Shape discriminated union
const Circle = Object({
type: Literal("circle"),
radius: Number,
center: Object({ x: Number, y: Number })
});
const Rectangle = Object({
type: Literal("rectangle"),
width: Number,
height: Number,
topLeft: Object({ x: Number, y: Number })
});
const Triangle = Object({
type: Literal("triangle"),
vertices: Array(Object({ x: Number, y: Number }))
});
const Shape = Union(Circle, Rectangle, Triangle);
type ShapeType = Static<typeof Shape>;
// { type: "circle", radius: number, center: { x: number, y: number } } |
// { type: "rectangle", width: number, height: number, topLeft: { x: number, y: number } } |
// { type: "triangle", vertices: { x: number, y: number }[] }
// Usage with type narrowing
function calculateArea(shape: unknown): number {
const validShape = Shape.check(shape);
switch (validShape.type) {
case "circle":
return Math.PI * validShape.radius ** 2;
case "rectangle":
return validShape.width * validShape.height;
case "triangle":
// Complex triangle area calculation
return 0; // simplified
}
}Use the built-in pattern matching functionality for unions.
import { Union, match, when, Literal, Object, String, Number } from "runtypes";
const Result = Union(
Object({ type: Literal("success"), data: String }),
Object({ type: Literal("error"), message: String, code: Number }),
Object({ type: Literal("loading") })
);
// Pattern matching
const handleResult = match(
when(Object({ type: Literal("success"), data: String }),
({ data }) => `Success: ${data}`),
when(Object({ type: Literal("error"), message: String, code: Number }),
({ message, code }) => `Error ${code}: ${message}`),
when(Object({ type: Literal("loading") }),
() => "Loading...")
);
// Usage
const response = handleResult({ type: "success", data: "Hello World" });
// "Success: Hello World"Validates values that match all of the provided runtypes simultaneously.
/**
* Creates a validator that requires values to match all provided runtypes
* @param intersectees - Array of runtypes, value must match all of them
* @example Intersect(Object({name: String}), Object({age: Number})).check({name: "Alice", age: 25})
*/
function Intersect<T extends readonly Runtype[]>(...intersectees: T): IntersectRuntype<T>;
interface IntersectRuntype<T> extends Runtype<IntersectType<T>> {
tag: "intersect";
intersectees: T;
}Usage Examples:
import { Intersect, Object, String, Number, Boolean } from "runtypes";
// Combining object types
const PersonBase = Object({
name: String,
age: Number
});
const ContactInfo = Object({
email: String,
phone: String.optional()
});
const PersonWithContact = Intersect(PersonBase, ContactInfo);
type PersonWithContactType = Static<typeof PersonWithContact>;
// {
// name: string;
// age: number;
// email: string;
// phone?: string;
// }
const person = PersonWithContact.check({
name: "Alice",
age: 25,
email: "alice@example.com",
phone: "555-0123"
});import { Intersect, Object, String, Number, Boolean } from "runtypes";
// Base entity
const BaseEntity = Object({
id: String,
createdAt: Number,
updatedAt: Number
});
// Audit trail
const Auditable = Object({
createdBy: String,
updatedBy: String,
version: Number
});
// Soft delete capability
const SoftDeletable = Object({
deleted: Boolean,
deletedAt: Number.optional(),
deletedBy: String.optional()
});
// User entity with mixins
const User = Intersect(
BaseEntity,
Auditable,
SoftDeletable,
Object({
name: String,
email: String,
role: Union(Literal("admin"), Literal("user"))
})
);
type UserType = Static<typeof User>;
// Combines all properties from all intersected typesimport { Union, Object, Literal, String, Unknown } from "runtypes";
const AsyncResult = Union(
Object({
status: Literal("pending")
}),
Object({
status: Literal("fulfilled"),
value: Unknown
}),
Object({
status: Literal("rejected"),
reason: String
})
);
// Usage in async contexts
async function handleAsyncResult(result: unknown) {
const validResult = AsyncResult.check(result);
if (validResult.status === "fulfilled") {
console.log("Success:", validResult.value);
} else if (validResult.status === "rejected") {
console.error("Error:", validResult.reason);
} else {
console.log("Still pending...");
}
}import { Union, Object, Literal, String, Number, Array } from "runtypes";
// API endpoint responses
const UserResponse = Object({
type: Literal("user"),
data: Object({
id: Number,
name: String,
email: String
})
});
const PostResponse = Object({
type: Literal("post"),
data: Object({
id: Number,
title: String,
content: String,
authorId: Number
})
});
const ListResponse = Object({
type: Literal("list"),
data: Array(Unknown),
pagination: Object({
page: Number,
total: Number,
hasMore: Boolean
})
});
const ApiResponse = Union(UserResponse, PostResponse, ListResponse);
// Type-safe response handling
function processResponse(response: unknown) {
const validResponse = ApiResponse.check(response);
switch (validResponse.type) {
case "user":
// TypeScript knows data is user object
return `User: ${validResponse.data.name}`;
case "post":
// TypeScript knows data is post object
return `Post: ${validResponse.data.title}`;
case "list":
// TypeScript knows about pagination
return `List: ${validResponse.data.length} items (page ${validResponse.pagination.page})`;
}
}import { Union, Intersect, Object, Literal, String, Number } from "runtypes";
// Base types
const Identifiable = Object({ id: String });
const Timestamped = Object({ timestamp: Number });
// Different entity types
const User = Object({ type: Literal("user"), name: String, email: String });
const Post = Object({ type: Literal("post"), title: String, content: String });
// Combine with mixins using intersection, then union for alternatives
const TimestampedUser = Intersect(User, Identifiable, Timestamped);
const TimestampedPost = Intersect(Post, Identifiable, Timestamped);
const Entity = Union(TimestampedUser, TimestampedPost);
type EntityType = Static<typeof Entity>;
// ({ type: "user", name: string, email: string } & { id: string } & { timestamp: number }) |
// ({ type: "post", title: string, content: string } & { id: string } & { timestamp: number })Install with Tessl CLI
npx tessl i tessl/npm-runtypes