@kypython/design-by-contract
v1.0.0
Published
A Design by Contract library for Node.js/TypeScript enforcing pre-conditions and post-conditions
Downloads
82
Maintainers
Readme
Design by Contract
A lightweight TypeScript library for enforcing Design by Contract principles in Node.js applications. Based on the methodology described in "The Pragmatic Programmer" by Andrew Hunt and David Thomas.
What is Design by Contract?
Design by Contract (DbC) is a programming methodology that formalizes the relationship between a function and its callers through contracts. These contracts specify:
- Pre-conditions: What must be true before a function is called (caller's obligations)
- Post-conditions: What will be true after a function returns (function's obligations)
This approach makes the assumptions and guarantees of your code explicit, leading to:
- Better error messages when contracts are violated
- Clearer documentation of function requirements
- Earlier detection of bugs
- Improved code reliability
Installation
npm install @kypython/design-by-contractQuick Start
import { contract } from '@kypython/design-by-contract';
const safeDivide = contract({
pre: (a, b) => b !== 0,
preMessage: 'Divisor must not be zero',
fn: (a: number, b: number) => a / b,
post: (result) => Number.isFinite(result),
postMessage: 'Result must be a finite number',
});
safeDivide(10, 2); // ✅ Returns 5
safeDivide(10, 0); // ❌ Throws ContractViolationError: preconditionNote: Due to TypeScript's reserved require identifier in CommonJS modules, the pre-condition function is named precondition. You can import it with your preferred name using:
import { precondition as require } from '@kypython/design-by-contract';API Reference
contract(config)
Wraps a function with contract enforcement. The wrapped function will enforce pre-conditions before execution and post-conditions after execution.
Parameters:
config.pre(optional): A predicate function that receives the function arguments and returnstrueif the pre-condition is metconfig.preMessage(optional): Custom error message for pre-condition failures (default: "Pre-condition failed")config.fn: The function to wrap with contract enforcementconfig.post(optional): A predicate function that receives the function result and returnstrueif the post-condition is metconfig.postMessage(optional): Custom error message for post-condition failures (default: "Post-condition failed")
Returns: The wrapped function with contract enforcement
Example:
const getFirst = contract({
pre: (arr: unknown[]) => arr.length > 0,
preMessage: 'Array must not be empty',
fn: <T>(arr: T[]): T => arr[0],
post: (result) => result !== undefined,
postMessage: 'Result must not be undefined',
});precondition(condition, message) (or alias as require)
Enforces a pre-condition. Throws ContractViolationError if the condition is false.
Note: Named precondition instead of require to avoid conflict with TypeScript's reserved require identifier. You can alias it:
import { precondition as require } from '@kypython/design-by-contract';Parameters:
condition: The condition that must be truemessage: Error message to display if condition fails
Example:
import { precondition as require } from '@kypython/design-by-contract';
function divide(a: number, b: number): number {
require(b !== 0, "Divisor must not be zero");
return a / b;
}ensure(condition, message)
Enforces a post-condition. Throws ContractViolationError if the condition is false.
Parameters:
condition: The condition that must be truemessage: Error message to display if condition fails
Example:
import { ensure } from 'design-by-contract';
function divide(a: number, b: number): number {
require(b !== 0, "Divisor must not be zero");
const result = a / b;
ensure(Number.isFinite(result), "Result must be a finite number");
return result;
}ContractViolationError
Custom error class thrown when contracts fail. It extends the standard Error class and includes:
type: Either'precondition'or'postcondition'message: The error message provided
Example:
import { ContractViolationError } from 'design-by-contract';
try {
safeDivide(10, 0);
} catch (error) {
if (error instanceof ContractViolationError) {
console.log(error.type); // 'precondition'
console.log(error.message); // 'Contract violation (precondition): ...'
}
}Examples
Example 1: Safe Division
import { contract } from '@kypython/design-by-contract';
const safeDivide = contract({
pre: (a: number, b: number) => b !== 0,
preMessage: 'Divisor must not be zero',
fn: (a: number, b: number) => a / b,
post: (result: number) => Number.isFinite(result),
postMessage: 'Result must be a finite number',
});Example 2: Array Operations
import { contract } from '@kypython/design-by-contract';
const createRange = contract({
pre: (start: number, end: number) => start <= end,
preMessage: 'Start must be less than or equal to end',
fn: (start: number, end: number): number[] => {
const result: number[] = [];
for (let i = start; i <= end; i++) {
result.push(i);
}
return result;
},
post: (result: number[]) => result.length > 0,
postMessage: 'Range must contain at least one element',
});Example 3: User Validation with Manual Assertions
import { precondition as require, ensure } from 'design-by-contract';
interface User {
name: string;
age: number;
email: string;
}
function createUser(name: string, age: number, email: string): User {
// Pre-conditions
require(name.trim().length > 0, 'Name must not be empty');
require(age >= 0 && age <= 150, 'Age must be between 0 and 150');
require(email.includes('@'), 'Email must contain @ symbol');
const user: User = {
name: name.trim(),
age,
email: email.toLowerCase(),
};
// Post-conditions
ensure(user.name.length > 0, 'User name must not be empty after creation');
ensure(user.email.includes('@'), 'User email must be valid');
return user;
}Usage Patterns
Pattern 1: Contract Wrapper
Use when you want declarative contract definition around a function:
const myFunction = contract({
pre: (arg1, arg2) => /* condition */,
fn: (arg1, arg2) => { /* implementation */ },
post: (result) => /* condition */,
});Pattern 2: Manual Assertions
Use when you need more control or want to check conditions inside the function body:
import { precondition as require, ensure } from 'design-by-contract';
function myFunction(arg1: Type1, arg2: Type2): ReturnType {
require(/* condition */, 'Pre-condition message');
// ... implementation ...
ensure(/* condition */, 'Post-condition message');
return result;
}Pattern 3: Mixed Approach
Combine both patterns for maximum flexibility:
const myFunction = contract({
pre: (arg) => /* basic validation */,
fn: (arg) => {
require(/* additional validation */, 'More specific message');
// ... implementation ...
},
post: (result) => /* validation */,
});Testing
npm testThe test suite includes:
- Unit tests for
require()andensure() - Unit tests for the
contract()wrapper - Integration tests with real-world scenarios
TypeScript Support
This library is written in TypeScript and provides full type safety. The contract() function preserves the original function's type signature, so you get full IntelliSense and type checking.
Best Practices
Use clear, descriptive messages: Your error messages should help developers understand what went wrong and why.
Pre-conditions check input validity: Pre-conditions should validate that the caller has met their obligations (e.g., passed valid arguments).
Post-conditions check output validity: Post-conditions should validate that your function has met its obligations (e.g., returned a valid result).
Don't use contracts for business logic: Contracts are for validation, not for implementing your application's core functionality.
Consider performance: Contract checks add overhead. Consider using them primarily in development/testing environments if performance is critical.
References
- The Pragmatic Programmer by Andrew Hunt and David Thomas
- Design by Contract on Wikipedia
- Eiffel Programming Language - The original language featuring Design by Contract
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
