Runtime validation for static types
—
Runtime validation for function arguments and return values. Contracts ensure type safety at function boundaries, making APIs more robust and providing clear runtime error messages.
Creates function contracts for runtime argument and return value validation.
/**
* Creates a function contract with optional argument and return validation
* @param options - Contract configuration
* @example Contract({ receives: Tuple(String, Number), returns: Boolean })
*/
function Contract<O extends ContractOptions>(options: O): ContractType<O>;
interface ContractOptions {
receives?: Runtype<readonly unknown[]>;
returns?: Runtype;
}
interface ContractType<O> {
receives?: O["receives"];
returns?: O["returns"];
enforce<F extends Function>(fn: F): EnforcedFunction<O, F>;
}Usage Examples:
import { Contract, String, Number, Boolean, Tuple } from "runtypes";
// Basic function contract
const AddContract = Contract({
receives: Tuple(Number, Number),
returns: Number
});
// Wrap function with contract
const safeAdd = AddContract.enforce((a: number, b: number) => a + b);
// Usage - validates arguments and return value
const result = safeAdd(5, 3); // 8
safeAdd("5", 3); // throws ValidationError - invalid arguments
// String processing contract
const StringProcessContract = Contract({
receives: Tuple(String),
returns: String
});
const processString = StringProcessContract.enforce((s: string) => s.toUpperCase());
const processed = processString("hello"); // "HELLO"import { Contract, Array, String, Object, Number } from "runtypes";
// Validate only arguments
const LogContract = Contract({
receives: Tuple(String, String) // level, message
});
const log = LogContract.enforce((level: string, message: string) => {
console.log(`[${level}] ${message}`);
});
log("INFO", "Application started"); // OK
log("INFO"); // throws ValidationError - missing argument
// Variable argument validation
const SumContract = Contract({
receives: Array(Number)
});
const sum = SumContract.enforce((...numbers: number[]) =>
numbers.reduce((acc, n) => acc + n, 0)
);
const total = sum(1, 2, 3, 4, 5); // 15import { Contract, String, Number, Object } from "runtypes";
// Validate only return value
const UserFactoryContract = Contract({
returns: Object({
id: Number,
name: String,
email: String
})
});
const createUser = UserFactoryContract.enforce(() => ({
id: 1,
name: "Alice",
email: "alice@example.com"
}));
const user = createUser(); // validated return value
// Contract catches bugs in implementation
const buggyCreateUser = UserFactoryContract.enforce(() => ({
id: "1", // wrong type - will throw ValidationError
name: "Alice",
email: "alice@example.com"
}));Creates contracts for async functions with Promise validation.
/**
* Creates a contract for async functions
* @param options - Async contract configuration
* @example AsyncContract({ receives: Tuple(String), resolves: Object({...}) })
*/
function AsyncContract<O extends AsyncContractOptions>(options: O): AsyncContractType<O>;
interface AsyncContractOptions {
receives?: Runtype<readonly unknown[]>;
returns?: Runtype;
resolves?: Runtype;
}
interface AsyncContractType<O> {
receives?: O["receives"];
returns?: O["returns"];
resolves?: O["resolves"];
enforce<F extends AsyncFunction>(fn: F): EnforcedAsyncFunction<O, F>;
}Usage Examples:
import { AsyncContract, String, Number, Object, Array } from "runtypes";
// API call contract
const FetchUserContract = AsyncContract({
receives: Tuple(Number), // user ID
resolves: Object({ // resolved value validation
id: Number,
name: String,
email: String
})
});
const fetchUser = FetchUserContract.enforce(async (id: number) => {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data; // validated against resolves runtype
});
// Usage
const user = await fetchUser(123); // Promise<{id: number, name: string, email: string}>
// Database operation contract
const SaveUserContract = AsyncContract({
receives: Tuple(Object({
name: String,
email: String
})),
resolves: Object({
id: Number,
name: String,
email: String,
createdAt: String
})
});
const saveUser = SaveUserContract.enforce(async (userData) => {
// Database save operation
const savedUser = await db.users.create(userData);
return savedUser;
});import { Contract, String, Number, Object, Boolean } from "runtypes";
class UserService {
private users: Map<number, any> = new Map();
// Contract for instance methods
getUserById = Contract({
receives: Tuple(Number),
returns: Object({
id: Number,
name: String,
active: Boolean
}).optional() // might return undefined
}).enforce((id: number) => {
return this.users.get(id);
});
createUser = Contract({
receives: Tuple(Object({
name: String,
email: String
})),
returns: Object({
id: Number,
name: String,
email: String,
active: Boolean
})
}).enforce((userData) => {
const id = this.users.size + 1;
const user = { ...userData, id, active: true };
this.users.set(id, user);
return user;
});
}
// Usage
const service = new UserService();
const user = service.createUser({ name: "Alice", email: "alice@example.com" });
const retrieved = service.getUserById(user.id);import { Contract, AsyncContract, String, Number, Object, Array, Union, Literal } from "runtypes";
// Request/Response contracts for API endpoints
const CreatePostContract = AsyncContract({
receives: Tuple(Object({
title: String,
content: String,
authorId: Number
})),
resolves: Object({
id: Number,
title: String,
content: String,
authorId: Number,
createdAt: String,
status: Union(Literal("draft"), Literal("published"))
})
});
const GetPostsContract = AsyncContract({
receives: Tuple(Object({
page: Number.optional(),
limit: Number.optional(),
authorId: Number.optional()
}).optional()),
resolves: Object({
posts: Array(Object({
id: Number,
title: String,
excerpt: String,
authorId: Number,
createdAt: String
})),
pagination: Object({
page: Number,
limit: Number,
total: Number,
hasMore: Boolean
})
})
});
// Implementation
const createPost = CreatePostContract.enforce(async (postData) => {
// Implementation details...
return await db.posts.create(postData);
});
const getPosts = GetPostsContract.enforce(async (options = {}) => {
// Implementation details...
return await db.posts.findMany(options);
});import { Contract, AsyncContract, String, Number, Object, Union, Literal } from "runtypes";
// Result pattern with contracts
const Result = <T, E>(success: Runtype<T>, error: Runtype<E>) => Union(
Object({ success: Literal(true), data: success }),
Object({ success: Literal(false), error: error })
);
const SafeDivisionContract = Contract({
receives: Tuple(Number, Number),
returns: Result(Number, String)
});
const safeDivision = SafeDivisionContract.enforce((a: number, b: number) => {
if (b === 0) {
return { success: false, error: "Division by zero" };
}
return { success: true, data: a / b };
});
// Usage
const result = safeDivision(10, 2);
if (result.success) {
console.log("Result:", result.data); // 5
} else {
console.error("Error:", result.error);
}
// Async error handling
const AsyncResultContract = AsyncContract({
receives: Tuple(String),
resolves: Result(Object({ id: Number, data: String }), String)
});
const fetchData = AsyncResultContract.enforce(async (url: string) => {
try {
const response = await fetch(url);
if (!response.ok) {
return { success: false, error: `HTTP ${response.status}` };
}
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
});import { Contract, String, Number, Object } from "runtypes";
// Base contracts for reuse
const UserData = Object({
name: String,
email: String,
age: Number
});
const UserWithId = UserData.extend({
id: Number,
createdAt: String
});
// Compose contracts from base types
const CreateUserContract = Contract({
receives: Tuple(UserData),
returns: UserWithId
});
const UpdateUserContract = Contract({
receives: Tuple(Number, UserData.asPartial()),
returns: UserWithId
});
const DeleteUserContract = Contract({
receives: Tuple(Number),
returns: Boolean
});
// Generic CRUD contract factory
const createCrudContracts = <T>(entityType: Runtype<T>, entityWithId: Runtype<T & {id: number}>) => ({
create: Contract({
receives: Tuple(entityType),
returns: entityWithId
}),
update: Contract({
receives: Tuple(Number, entityType.asPartial?.() || entityType),
returns: entityWithId
}),
delete: Contract({
receives: Tuple(Number),
returns: Boolean
}),
getById: Contract({
receives: Tuple(Number),
returns: entityWithId.optional?.() || entityWithId
})
});
// Usage
const userContracts = createCrudContracts(UserData, UserWithId);import { Contract, String, Number } from "runtypes";
// Contract with debugging
const DebugContract = Contract({
receives: Tuple(String, Number),
returns: String
});
const debugFunction = DebugContract.enforce((name: string, count: number) => {
console.log(`Function called with: ${name}, ${count}`);
const result = `${name} called ${count} times`;
console.log(`Function returning: ${result}`);
return result;
});
// Conditional contracts (for development vs production)
const createConditionalContract = (options: ContractOptions) => {
if (process.env.NODE_ENV === 'development') {
return Contract(options);
}
// Return pass-through in production
return {
enforce: <F extends Function>(fn: F) => fn
};
};
const OptimizedContract = createConditionalContract({
receives: Tuple(String),
returns: String
});Install with Tessl CLI
npx tessl i tessl/npm-runtypes