0
# Function Contracts
1
2
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.
3
4
## Capabilities
5
6
### Contract
7
8
Creates function contracts for runtime argument and return value validation.
9
10
```typescript { .api }
11
/**
12
* Creates a function contract with optional argument and return validation
13
* @param options - Contract configuration
14
* @example Contract({ receives: Tuple(String, Number), returns: Boolean })
15
*/
16
function Contract<O extends ContractOptions>(options: O): ContractType<O>;
17
18
interface ContractOptions {
19
receives?: Runtype<readonly unknown[]>;
20
returns?: Runtype;
21
}
22
23
interface ContractType<O> {
24
receives?: O["receives"];
25
returns?: O["returns"];
26
enforce<F extends Function>(fn: F): EnforcedFunction<O, F>;
27
}
28
```
29
30
**Usage Examples:**
31
32
```typescript
33
import { Contract, String, Number, Boolean, Tuple } from "runtypes";
34
35
// Basic function contract
36
const AddContract = Contract({
37
receives: Tuple(Number, Number),
38
returns: Number
39
});
40
41
// Wrap function with contract
42
const safeAdd = AddContract.enforce((a: number, b: number) => a + b);
43
44
// Usage - validates arguments and return value
45
const result = safeAdd(5, 3); // 8
46
safeAdd("5", 3); // throws ValidationError - invalid arguments
47
48
// String processing contract
49
const StringProcessContract = Contract({
50
receives: Tuple(String),
51
returns: String
52
});
53
54
const processString = StringProcessContract.enforce((s: string) => s.toUpperCase());
55
const processed = processString("hello"); // "HELLO"
56
```
57
58
### Argument Validation Only
59
60
```typescript
61
import { Contract, Array, String, Object, Number } from "runtypes";
62
63
// Validate only arguments
64
const LogContract = Contract({
65
receives: Tuple(String, String) // level, message
66
});
67
68
const log = LogContract.enforce((level: string, message: string) => {
69
console.log(`[${level}] ${message}`);
70
});
71
72
log("INFO", "Application started"); // OK
73
log("INFO"); // throws ValidationError - missing argument
74
75
// Variable argument validation
76
const SumContract = Contract({
77
receives: Array(Number)
78
});
79
80
const sum = SumContract.enforce((...numbers: number[]) =>
81
numbers.reduce((acc, n) => acc + n, 0)
82
);
83
84
const total = sum(1, 2, 3, 4, 5); // 15
85
```
86
87
### Return Value Validation Only
88
89
```typescript
90
import { Contract, String, Number, Object } from "runtypes";
91
92
// Validate only return value
93
const UserFactoryContract = Contract({
94
returns: Object({
95
id: Number,
96
name: String,
97
email: String
98
})
99
});
100
101
const createUser = UserFactoryContract.enforce(() => ({
102
id: 1,
103
name: "Alice",
104
email: "alice@example.com"
105
}));
106
107
const user = createUser(); // validated return value
108
109
// Contract catches bugs in implementation
110
const buggyCreateUser = UserFactoryContract.enforce(() => ({
111
id: "1", // wrong type - will throw ValidationError
112
name: "Alice",
113
email: "alice@example.com"
114
}));
115
```
116
117
### AsyncContract
118
119
Creates contracts for async functions with Promise validation.
120
121
```typescript { .api }
122
/**
123
* Creates a contract for async functions
124
* @param options - Async contract configuration
125
* @example AsyncContract({ receives: Tuple(String), resolves: Object({...}) })
126
*/
127
function AsyncContract<O extends AsyncContractOptions>(options: O): AsyncContractType<O>;
128
129
interface AsyncContractOptions {
130
receives?: Runtype<readonly unknown[]>;
131
returns?: Runtype;
132
resolves?: Runtype;
133
}
134
135
interface AsyncContractType<O> {
136
receives?: O["receives"];
137
returns?: O["returns"];
138
resolves?: O["resolves"];
139
enforce<F extends AsyncFunction>(fn: F): EnforcedAsyncFunction<O, F>;
140
}
141
```
142
143
**Usage Examples:**
144
145
```typescript
146
import { AsyncContract, String, Number, Object, Array } from "runtypes";
147
148
// API call contract
149
const FetchUserContract = AsyncContract({
150
receives: Tuple(Number), // user ID
151
resolves: Object({ // resolved value validation
152
id: Number,
153
name: String,
154
email: String
155
})
156
});
157
158
const fetchUser = FetchUserContract.enforce(async (id: number) => {
159
const response = await fetch(`/api/users/${id}`);
160
const data = await response.json();
161
return data; // validated against resolves runtype
162
});
163
164
// Usage
165
const user = await fetchUser(123); // Promise<{id: number, name: string, email: string}>
166
167
// Database operation contract
168
const SaveUserContract = AsyncContract({
169
receives: Tuple(Object({
170
name: String,
171
email: String
172
})),
173
resolves: Object({
174
id: Number,
175
name: String,
176
email: String,
177
createdAt: String
178
})
179
});
180
181
const saveUser = SaveUserContract.enforce(async (userData) => {
182
// Database save operation
183
const savedUser = await db.users.create(userData);
184
return savedUser;
185
});
186
```
187
188
## Advanced Contract Patterns
189
190
### Method Contracts
191
192
```typescript
193
import { Contract, String, Number, Object, Boolean } from "runtypes";
194
195
class UserService {
196
private users: Map<number, any> = new Map();
197
198
// Contract for instance methods
199
getUserById = Contract({
200
receives: Tuple(Number),
201
returns: Object({
202
id: Number,
203
name: String,
204
active: Boolean
205
}).optional() // might return undefined
206
}).enforce((id: number) => {
207
return this.users.get(id);
208
});
209
210
createUser = Contract({
211
receives: Tuple(Object({
212
name: String,
213
email: String
214
})),
215
returns: Object({
216
id: Number,
217
name: String,
218
email: String,
219
active: Boolean
220
})
221
}).enforce((userData) => {
222
const id = this.users.size + 1;
223
const user = { ...userData, id, active: true };
224
this.users.set(id, user);
225
return user;
226
});
227
}
228
229
// Usage
230
const service = new UserService();
231
const user = service.createUser({ name: "Alice", email: "alice@example.com" });
232
const retrieved = service.getUserById(user.id);
233
```
234
235
### API Endpoint Contracts
236
237
```typescript
238
import { Contract, AsyncContract, String, Number, Object, Array, Union, Literal } from "runtypes";
239
240
// Request/Response contracts for API endpoints
241
const CreatePostContract = AsyncContract({
242
receives: Tuple(Object({
243
title: String,
244
content: String,
245
authorId: Number
246
})),
247
resolves: Object({
248
id: Number,
249
title: String,
250
content: String,
251
authorId: Number,
252
createdAt: String,
253
status: Union(Literal("draft"), Literal("published"))
254
})
255
});
256
257
const GetPostsContract = AsyncContract({
258
receives: Tuple(Object({
259
page: Number.optional(),
260
limit: Number.optional(),
261
authorId: Number.optional()
262
}).optional()),
263
resolves: Object({
264
posts: Array(Object({
265
id: Number,
266
title: String,
267
excerpt: String,
268
authorId: Number,
269
createdAt: String
270
})),
271
pagination: Object({
272
page: Number,
273
limit: Number,
274
total: Number,
275
hasMore: Boolean
276
})
277
})
278
});
279
280
// Implementation
281
const createPost = CreatePostContract.enforce(async (postData) => {
282
// Implementation details...
283
return await db.posts.create(postData);
284
});
285
286
const getPosts = GetPostsContract.enforce(async (options = {}) => {
287
// Implementation details...
288
return await db.posts.findMany(options);
289
});
290
```
291
292
### Error Handling with Contracts
293
294
```typescript
295
import { Contract, AsyncContract, String, Number, Object, Union, Literal } from "runtypes";
296
297
// Result pattern with contracts
298
const Result = <T, E>(success: Runtype<T>, error: Runtype<E>) => Union(
299
Object({ success: Literal(true), data: success }),
300
Object({ success: Literal(false), error: error })
301
);
302
303
const SafeDivisionContract = Contract({
304
receives: Tuple(Number, Number),
305
returns: Result(Number, String)
306
});
307
308
const safeDivision = SafeDivisionContract.enforce((a: number, b: number) => {
309
if (b === 0) {
310
return { success: false, error: "Division by zero" };
311
}
312
return { success: true, data: a / b };
313
});
314
315
// Usage
316
const result = safeDivision(10, 2);
317
if (result.success) {
318
console.log("Result:", result.data); // 5
319
} else {
320
console.error("Error:", result.error);
321
}
322
323
// Async error handling
324
const AsyncResultContract = AsyncContract({
325
receives: Tuple(String),
326
resolves: Result(Object({ id: Number, data: String }), String)
327
});
328
329
const fetchData = AsyncResultContract.enforce(async (url: string) => {
330
try {
331
const response = await fetch(url);
332
if (!response.ok) {
333
return { success: false, error: `HTTP ${response.status}` };
334
}
335
const data = await response.json();
336
return { success: true, data };
337
} catch (error) {
338
return { success: false, error: error.message };
339
}
340
});
341
```
342
343
### Contract Composition
344
345
```typescript
346
import { Contract, String, Number, Object } from "runtypes";
347
348
// Base contracts for reuse
349
const UserData = Object({
350
name: String,
351
email: String,
352
age: Number
353
});
354
355
const UserWithId = UserData.extend({
356
id: Number,
357
createdAt: String
358
});
359
360
// Compose contracts from base types
361
const CreateUserContract = Contract({
362
receives: Tuple(UserData),
363
returns: UserWithId
364
});
365
366
const UpdateUserContract = Contract({
367
receives: Tuple(Number, UserData.asPartial()),
368
returns: UserWithId
369
});
370
371
const DeleteUserContract = Contract({
372
receives: Tuple(Number),
373
returns: Boolean
374
});
375
376
// Generic CRUD contract factory
377
const createCrudContracts = <T>(entityType: Runtype<T>, entityWithId: Runtype<T & {id: number}>) => ({
378
create: Contract({
379
receives: Tuple(entityType),
380
returns: entityWithId
381
}),
382
383
update: Contract({
384
receives: Tuple(Number, entityType.asPartial?.() || entityType),
385
returns: entityWithId
386
}),
387
388
delete: Contract({
389
receives: Tuple(Number),
390
returns: Boolean
391
}),
392
393
getById: Contract({
394
receives: Tuple(Number),
395
returns: entityWithId.optional?.() || entityWithId
396
})
397
});
398
399
// Usage
400
const userContracts = createCrudContracts(UserData, UserWithId);
401
```
402
403
### Performance and Debugging
404
405
```typescript
406
import { Contract, String, Number } from "runtypes";
407
408
// Contract with debugging
409
const DebugContract = Contract({
410
receives: Tuple(String, Number),
411
returns: String
412
});
413
414
const debugFunction = DebugContract.enforce((name: string, count: number) => {
415
console.log(`Function called with: ${name}, ${count}`);
416
const result = `${name} called ${count} times`;
417
console.log(`Function returning: ${result}`);
418
return result;
419
});
420
421
// Conditional contracts (for development vs production)
422
const createConditionalContract = (options: ContractOptions) => {
423
if (process.env.NODE_ENV === 'development') {
424
return Contract(options);
425
}
426
// Return pass-through in production
427
return {
428
enforce: <F extends Function>(fn: F) => fn
429
};
430
};
431
432
const OptimizedContract = createConditionalContract({
433
receives: Tuple(String),
434
returns: String
435
});
436
```