xdbc
v1.0.220
Published
A Typescript Design by Contract Framework
Maintainers
Readme
XDBC — eXplicit Design by Contract for TypeScript
A decorator-based Design by Contract framework that enforces preconditions, postconditions, and invariants through TypeScript metadata — delivering precise, self-documenting, and verifiable component contracts.
At a Glance
| Approach | With XDBC | Without XDBC | |---|---|---| | Parameter validation | @DBC.ParamvalueProvidermethod(@REGEX.PRE(/^.*XDBC.*$/i) input: string[]) { ...} | method(input: string[]) { input.forEach((el, i) => { console.assert(/^.*XDBC.*$/i.test(el), "error"); }); ...} | | Field invariant | @REGEX.INVARIANT(/^.*XDBC.*$/i)public field = "XDBC"; | get field(): string { return this._field; }set field(v: string) { console.assert(/^.*XDBC.*$/i.test(v), "error"); this._field = v;} | | Return validation | @REGEX.POST(/^XDBC$/i)method(input: unknown): string { ... return result;} | method(input: unknown): string { ... if (!/^XDBC$/i.test(result)) { throw new Error("error"); } return result;} |
Contract violations produce structured, actionable diagnostics:
[ XDBC Infringement [ From "method" in "MyClass": [ Parameter-value "+1d,+5d,-x10y"
of the 1st parameter did not fulfill one of it's contracts: Violating-Arrayelement at
index 2. Value has to comply to regular expression "/^(?i:(NOW)|([+-]\d+[dmy]))$/i"]]]Table of Contents
- What is Design by Contract?
- Why XDBC?
- Installation
- Decorator API
- Quick Start
- Contracts Reference
- Core Concepts
- Advanced Features
- DOM / HTML Input Binding
- Configuration
- API Documentation
- Built With XDBC
- Contributing
- License
What is Design by Contract?
Design by Contract (DbC) is a software engineering methodology that defines formal, precise, and verifiable interface specifications for software components. Each component's contract comprises:
| Element | Purpose | |---|---| | Preconditions | Conditions that must hold true before a method executes | | Postconditions | Guarantees that must hold true after a method returns | | Invariants | Properties that must remain true throughout an object's lifetime |
DbC vs. Assertions
| Aspect | XDBC Decorators | Manual Assertions |
|---|---|---|
| Formality | Formal, declarative, co-located with signatures | Informal, scattered through method bodies |
| Integration | TypeScript metadata and decorators | Built-in console.assert / throw |
| Expressiveness | Composable, parameterized contract objects | Simple boolean checks |
| Readability | Contracts are visible at the API surface | Validation logic obscures business logic |
| Maintainability | Contracts are localized and reusable | Duplicated checks are hard to track |
| Production control | Selectively enable/disable by contract type | Typically all-or-nothing |
Why XDBC?
- Declarative — contracts live as decorators alongside type signatures, not buried in method bodies
- Composable — combine contracts with
AE,OR, andIFfor expressive validation - Configurable — toggle preconditions, postconditions, and invariants independently
- Diagnostic — structured error messages pinpoint the exact violation, parameter, and context
- Extensible — 16 built-in contracts, with support for custom contracts and Zod schema integration
- Zero runtime overhead — disable contract checking in production with a single flag
Installation
npm install xdbcRequirements: TypeScript 5.x with experimentalDecorators and emitDecoratorMetadata enabled in tsconfig.json.
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Decorator API
XDBC is built on TypeScript's legacy (experimentalDecorators) decorator API — not the TC39 Stage 3 decorator API.
Why? Stage 3 decorators deliberately excluded parameter decorators from their scope. Parameter decorators are the foundation of XDBC's contract syntax: @DEFINED.PRE(), @GREATER.PRE(0), and every other PRE contract applied per-parameter depends on them. There is no equivalent in Stage 3, and no workaround that preserves the same ergonomics.
Is this unusual? No. Some of the most widely adopted TypeScript frameworks in the industry require experimentalDecorators for exactly the same reason and have no near-term plans to migrate: on the backend, NestJS (used at thousands of companies, tens of millions of weekly downloads), TypeORM, class-validator, and class-transformer; on the frontend, vue-class-component (Vue's official class-based API) and MobX (the dominant React state management library for class-based stores). experimentalDecorators: true is compatible with Angular, Vue, and React — it is a TypeScript compiler flag, not a framework-level constraint, and does not conflict with any framework's runtime behavior. Any project already using these libraries has experimentalDecorators: true in its tsconfig and can adopt XDBC with zero additional configuration. For projects that don't, enabling it requires adding two lines to tsconfig.json — see Installation.
Is it risky? No. TypeScript explicitly supports both APIs simultaneously and has made no announcement about removing experimentalDecorators. Any project already using the frameworks above already has experimentalDecorators: true in its tsconfig, meaning XDBC requires zero additional configuration in those environments.
What about the future? TC39 has an active Stage 1 proposal — Class Method Parameter Decorators (Ron Buckton, 2023) — that would close this gap. When parameter decorators reach a stable stage, XDBC will migrate to Stage 3. Because all decorator wiring is contained in DBC.ts and the 17 contract classes are completely insulated from it, that migration will be localized and non-breaking at the API level.
Quick Start
import { DBC, REGEX, TYPE, EQ } from "xdbc";
class UserService {
// Invariant: email must always match pattern
@REGEX.INVARIANT(/^[^@]+@[^@]+\.[^@]+$/)
public email = "[email protected]";
// Precondition: name must be a string; Postcondition: return must match pattern
@REGEX.POST(/^Hello, .+$/)
@DBC.ParamvalueProvider
public greet(@TYPE.PRE("string") name: string): string {
return `Hello, ${name}`;
}
// Precondition: age must be >= 0
@DBC.ParamvalueProvider
public setAge(@GREATER_OR_EQUAL.PRE(0) age: number) {
// ...
}
}Contracts Reference
XDBC ships with 16 contracts organized into core validators and derived specializations:
Core Contracts
| Contract | Description | Constructor |
|---|---|---|
| REGEX | Value must match a regular expression | new REGEX(expression: RegExp) |
| TYPE | Value must be of a specified type (supports pipe-separated: "string\|number") | new TYPE(type: string) |
| EQ | Value must equal (or not equal) a reference value | new EQ(equivalent: any, invert?: boolean) |
| COMPARISON | Numeric comparison against a reference value | new COMPARISON(equivalent, equalityPermitted, invert) |
| INSTANCE | Value must be an instance of a specified class | new INSTANCE(reference: any \| any[]) |
| AE | Every element in an array must satisfy a set of contracts | new AE(conditions, index?, idxEnd?) |
| OR | At least one of a set of contracts must be satisfied | new OR(conditions: DBC[]) |
| IF | Conditional contract: if A holds, then B must also hold | IF.PRE(condition, inCase, path?, invert?) |
| JSON_OP | Object must contain specific properties of specific types | new JSON_OP(properties: {name, type}[], checkElements?) |
| JSON_Parse | String must be valid JSON; optionally forwards parsed result | new JSON_Parse(receptor?: (json) => void) |
| DEFINED | Value must not be null or undefined | — |
| UNDEFINED | Value must be undefined | — |
| ARRAY | Value must be an array | — |
| HasAttribute | HTMLElement must possess a named attribute | HasAttribute.PRE(attrName, invert?) |
| ZOD | Value must validate against a Zod schema | new ZOD(schema: z.ZodType) |
Derived Contracts
| Contract | Derives From | Semantics |
|---|---|---|
| GREATER | COMPARISON | value > reference |
| GREATER_OR_EQUAL | COMPARISON | value >= reference |
| LESS | COMPARISON | value < reference |
| LESS_OR_EQUAL | COMPARISON | value <= reference |
| DIFFERENT | EQ | value !== reference |
| PLAIN_OBJECT | ARRAY | Value must be a non-null, non-array object |
Built-in Regular Expressions
REGEX.stdExp provides ready-to-use patterns:
| Key | Validates |
|---|---|
| htmlAttributeName | HTML attribute names |
| eMail | Email addresses |
| property | Property identifiers |
| url | URLs |
| keyPath | Key paths |
| date | Date strings |
| dateFormat | Date format patterns |
| cssSelector | CSS selectors |
| boolean | Boolean string literals |
| colorCodeHEX | Hex color codes |
| simpleHotkey | Keyboard shortcuts |
| bcp47 | BCP 47 language tags |
Core Concepts
Decorator Types
Every contract exposes three decorator factories:
| Decorator | Applies To | Validates |
|---|---|---|
| Contract.PRE(...) | Method parameters | Input values before method execution |
| Contract.POST(...) | Methods | Return value after method execution |
| Contract.INVARIANT(...) | Fields / Properties | Value on every assignment (including initialization) |
The ParamvalueProvider Decorator
TypeScript parameter decorators do not natively receive parameter values. Any method using PRE parameter contracts must be decorated with @DBC.ParamvalueProvider:
@DBC.ParamvalueProvider
public process(
@TYPE.PRE("string") name: string,
@REGEX.PRE(/^\d{4}$/) code: string
) { ... }Path Resolution
All PRE, POST, and INVARIANT decorators accept an optional path parameter — a dotted path that specifies a nested property of the value to validate instead of the value itself:
// Validate that element.tagName === "SELECT"
@DBC.ParamvalueProvider
public handleElement(@EQ.PRE("SELECT", false, "tagName") el: HTMLElement) { }
// Validate that value.length === 1
@EQ.INVARIANT(1, false, "length")
public singleChar = "X";Path resolution supports:
- Dot notation:
"user.address.city" - Array indices:
"items[0]" - Method calls:
"getName()" - HTML attributes:
"@data-id"
Custom Hints
Add context to error messages with the optional hint parameter:
@DBC.ParamvalueProvider
public setAge(
@GREATER_OR_EQUAL.PRE(0, undefined, "Age must be non-negative") age: number
) { }Advanced Features
Composing Contracts with AE
Validate array elements against one or more contracts, optionally targeting a specific index range:
// All elements must be non-empty strings matching a date pattern
@AE.PRE([new TYPE("string"), new REGEX(/^\d{4}-\d{2}-\d{2}$/)])
public processDates(dates: string[]) { }
// Only elements at indices 1 through 3
@AE.PRE([new REGEX(/^[A-Z]+$/)], 1, 3)
public processRange(items: string[]) { }
// Single element at index 0
@AE.PRE([new TYPE("number")], 0)
public processFirst(values: unknown[]) { }Logical Composition with OR
At least one contract must pass:
@DBC.ParamvalueProvider
public setStatus(@OR.PRE([new EQ("active"), new EQ("inactive"), new EQ("pending")]) status: string) { }Conditional Contracts with IF
Apply a contract only when a precondition holds:
// If the value is a string, it must also match digits-only
@IF.PRE(new TYPE("string"), new REGEX(/^\d+$/))
public processInput(value: unknown) { }Zod Schema Integration
Leverage Zod schemas for complex structural validation:
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive()
});
@DBC.ParamvalueProvider
public createUser( @ZOD.PRE(UserSchema) data: unknown) { }Type-Safe Static Checks
Several contracts offer static tsCheck methods for imperative validation outside of decorators:
// Throws if value is not a string
const name = TYPE.tsCheck<string>(input, "string", "Expected a string");
// Throws if value doesn't match regex
const code = REGEX.tsCheck<string>(input, /^\d{4}$/, "Invalid code format");
// Throws if not an instance of Date
const date = INSTANCE.tsCheck<Date>(input, Date, "Expected a Date");
// Throws if none of the conditions pass
const result = OR.tsCheck<string>(input, [new EQ("a"), new EQ("b")]);DOM / HTML Input Binding
XDBC can enforce contracts directly on <input> and <textarea> elements using HTML data attributes — no JavaScript wiring required per element.
Setup
import { scanDOM } from "xdbc/DBC/DOM";
// Call once after the DOM is ready. Returns a cleanup function.
const cleanup = scanDOM();
// Optionally scope to a subtree:
const cleanup = scanDOM(document.getElementById("my-form"));
// Remove all listeners (e.g. on component unmount):
cleanup();Opting an element in
Any data-xdbc-* attribute is sufficient to enroll an element — no data-xdbc marker is required:
<input data-xdbc-regex="^\d*$" />The optional data-xdbc attribute specifies a custom DBC instance path (default: "WaXCode.DBC"):
<input data-xdbc="MyApp.DBC" data-xdbc-regex="^\d*$" />Built-in contract attributes
| Attribute | Example value | Contract |
|---|---|---|
| data-xdbc-regex | ^\d*$ | REGEX |
| data-xdbc-type | string\|number | TYPE |
| data-xdbc-eq | hello | EQ |
| data-xdbc-different | forbidden | EQ (inverted) |
| data-xdbc-defined | (no value needed) | DEFINED |
| data-xdbc-undefined | (no value needed) | UNDEFINED |
| data-xdbc-greater | 5 | COMPARISON |
| data-xdbc-greater-or-equal | 5 | COMPARISON |
| data-xdbc-less | 100 | COMPARISON |
| data-xdbc-less-or-equal | 100 | COMPARISON |
| data-xdbc-or | regex:^\d+$;;eq:N/A | OR combinator (see below) |
Multiple attributes on one element are all enforced — the first failure blocks and reports.
OR fragment syntax
Use data-xdbc-or to express that the value must satisfy at least one of several contracts. Fragments are separated by ;;; each fragment is <contract-key>:<value>, where the split is on the first : only (so colons inside regex patterns are safe):
<!-- digits, OR exactly the string "N/A" -->
<input data-xdbc-or="regex:^\d+$;;eq:N/A" />
<!-- http or https URL, OR the literal "N/A" -->
<input data-xdbc-or="regex:^https?://;;eq:N/A" />Keystroke vs. blur validation
By default, contracts fire on blur (when the element loses focus), so partially typed values are never rejected mid-entry.
To switch an element to keystroke-time validation, set data-xdbc-validate-on="input":
<!-- validates after every keystroke -->
<input data-xdbc-validate-on="input" data-xdbc-regex="^\d*$" />Every built-in contract also has an -input twin that always fires on keystroke, regardless of data-xdbc-validate-on. Use it alongside the base contract to apply a permissive pattern while the user is typing and a strict pattern on blur:
| Base attribute | -input twin | When it fires |
|---|---|---|
| data-xdbc-regex | data-xdbc-regex-input | every keystroke |
| data-xdbc-type | data-xdbc-type-input | every keystroke |
| data-xdbc-eq | data-xdbc-eq-input | every keystroke |
| data-xdbc-different | data-xdbc-different-input | every keystroke |
| data-xdbc-defined | data-xdbc-defined-input | every keystroke |
| data-xdbc-undefined | data-xdbc-undefined-input | every keystroke |
| data-xdbc-greater | data-xdbc-greater-input | every keystroke |
| data-xdbc-greater-or-equal | data-xdbc-greater-or-equal-input | every keystroke |
| data-xdbc-less | data-xdbc-less-input | every keystroke |
| data-xdbc-less-or-equal | data-xdbc-less-or-equal-input | every keystroke |
| data-xdbc-or | data-xdbc-or-input | every keystroke |
This is the recommended pattern for fields where the fully valid value can only be determined once typing is complete — such as email addresses, domain names, or structured codes:
<!-- strict LDAP DN on blur; only legal characters allowed on every keystroke -->
<input
data-xdbc-regex="^[A-Za-z]+=.+(,[A-Za-z]+=.+)*$"
data-xdbc-regex-input="^[a-zA-Z0-9=,. _\-]*$"
/>Real-world example — Angular with toast notifications
In production, two or three attributes on an ordinary <input> — combined with a single global onInfringement callback — are enough to guarantee correctness and surface precise, human-readable error messages through your application's own notification system.
The following field is from a school Active Directory management suite. It binds a full LDAP Distinguished Name contract to a PrimeNG input with zero custom validator code:
<!-- ldap-settings.component.html -->
<input
pInputText
[(ngModel)]="form.base_dn"
placeholder="DC=school,DC=local"
data-xdbc-regex="^[A-Za-z]+=.+(,[A-Za-z]+=.+)*$"
data-xdbc-regex-input="^[a-zA-Z0-9=,. _\-]*$"
/>Two attributes do all the work:
data-xdbc-regex-input— permits only the characters that can legally appear in an LDAP DN while the user is still typing, so the field never blocks a partial entry.data-xdbc-regex— enforces the fullkey=value(,key=value)*DN structure on blur, giving the user a complete value to correct.
Route violations to PrimeNG's MessageService (or any toast library) once at startup:
// app.component.ts
import { scanDOM } from "xdbc/DBC/DOM";
export class AppComponent implements OnInit {
constructor(private messages: MessageService) {}
ngOnInit() {
const dbc = (globalThis as any).WaXCode.DBC;
dbc.infringementSettings.onInfringement = (infringement: Error) => {
this.messages.add({
severity: "error",
summary: "Invalid input",
detail: infringement.message,
});
};
scanDOM();
}
}No per-element wiring. No Angular validators. Three attributes on an HTML element and one global callback are enough to enforce correctness and deliver structured diagnostics directly to your users.
Behaviour on infringement
- The element's value is reverted to the last accepted state, blocking the invalid input.
- The DBC instance's
onInfringement,logToConsole, andthrowExceptionsettings are all honoured. Any throw is swallowed inside the event handler so it cannot propagate unhandled.
IME / composition awareness
Validation is suspended during IME composition (e.g. CJK on-screen keyboards) and runs once on compositionend, so partially composed characters are never incorrectly rejected.
Registering custom contracts
Use registerDOMContract to add any contract — including future ones — without modifying the library:
import { registerDOMContract } from "xdbc/DBC/DOM";
import { MY_CONTRACT } from "./MY_CONTRACT";
// Register once, before scanDOM():
registerDOMContract("my-contract", (value, attrValue) =>
MY_CONTRACT.checkAlgorithm(value, attrValue),
);<input data-xdbc-my-contract="someConfig" />The attrValue string is whatever appears in the attribute — parse it however your contract needs.
Configuration
DBC Instance Settings
A default DBC instance is automatically registered at WaXCode.DBC when the module is imported. You can access and configure it:
import { DBC } from "xdbc";
// Access the default DBC instance
const dbc = (globalThis as any).WaXCode.DBC as DBC;
// Toggle contract checking
dbc.executionSettings.checkPreconditions = true;
dbc.executionSettings.checkPostconditions = true;
dbc.executionSettings.checkInvariants = true;
// Configure infringement handling
dbc.infringementSettings.throwException = true; // throw DBC.Infringement on violation
dbc.infringementSettings.logToConsole = false; // log to console instead
# React to infringements programmatically
dbc.infringementSettings.onInfringement = (infringement, context) => {
// infringement — DBC.Infringement instance (extends Error, has .message and .stack)
// context.type — "precondition" | "postcondition" | "invariant"
// context.value — the raw value that violated the contract
Sentry.captureException(infringement, { extra: context });
};The callback fires before throwException, so it always runs even when an exception is thrown. All three settings are independent and can be combined freely.
Multiple DBC Instances
Create isolated DBC instances with separate configurations using DBC.register():
// Register a vendor-specific instance at a custom path
const vendorDbc = new DBC(
{ throwException: false, logToConsole: true },
);
DBC.register(vendorDbc, "MyVendor.DBC");
// Route a contract to the custom instance via its path
@REGEX.INVARIANT(/^[A-Z]+$/, undefined, undefined, "MyVendor.DBC")
public code = "ABC";Note:
new DBC()does not automatically mount ontoglobalThis. CallDBC.register(instance, path)to make an instance available for decorator resolution.
Test Isolation
Use DBC.isolated() to run tests with a temporary DBC instance that doesn't affect other tests:
DBC.isolated((tempDbc) => {
// tempDbc is registered at "WaXCode.DBC" for the duration of this callback
tempDbc.executionSettings.checkPreconditions = false;
// ... run tests with contracts disabled ...
});
// Original DBC instance is automatically restored hereDisabling Contracts in Production
const dbc = (globalThis as any).WaXCode.DBC as DBC;
dbc.executionSettings.checkPreconditions = false;
dbc.executionSettings.checkPostconditions = false;
dbc.executionSettings.checkInvariants = false;API Documentation
Full generated API documentation is available at callaris.github.io/XDBC.
See Demo.ts for annotated usage examples.
Built With XDBC
XDBC is actively used in production across the following projects:
| Project | Context | |---|---| | CodBi | Low-code engine plugin for XIMA Formcycle | | tinymce-multicloud-plugin | multiCloud plugin for TinyMCE | | (internal) | Comprehensive Active Directory management suite for schools, deployed at a German public administration |
XDBC is used in the Angular frontends of the above projects.
Contributing
Participation is highly valued and warmly welcomed. The ultimate goal is to create a tool that proves genuinely useful and empowers a wide range of developers to build more robust and reliable applications.
Please see CONTRIBUTING.md for guidelines, and CODE_OF_CONDUCT.md for community standards.
License
MIT © Callari, Salvatore
"Design by Contract" is a registered trademark of Eiffel Software. XDBC is an independent project and is not affiliated with or endorsed by Eiffel Software.
