0
# Refinement & Branding
1
2
Advanced type construction for adding runtime constraints and creating branded types.
3
4
## Capabilities
5
6
### Refinement Function
7
8
Add runtime constraints to existing codecs with custom predicate functions.
9
10
```typescript { .api }
11
/**
12
* Add a refinement predicate to an existing codec
13
* @param codec - The base codec to refine
14
* @param predicate - Predicate function for additional validation
15
* @param name - Optional name for the refined codec
16
*/
17
function refinement<C extends Any, B extends TypeOf<C>>(
18
codec: C,
19
refinement: Refinement<TypeOf<C>, B>,
20
name?: string
21
): RefinementC<C, B>;
22
23
function refinement<C extends Any>(
24
codec: C,
25
predicate: Predicate<TypeOf<C>>,
26
name?: string
27
): RefinementC<C>;
28
29
interface RefinementC<C extends Any, B = TypeOf<C>> extends RefinementType<C, B, OutputOf<C>, InputOf<C>> {}
30
31
/** Predicate function type */
32
type Predicate<A> = (a: A) => boolean;
33
34
/** Refinement function type */
35
type Refinement<A, B extends A> = (a: A) => a is B;
36
```
37
38
**Usage Examples:**
39
40
```typescript
41
import * as t from "io-ts";
42
43
// Create a positive number codec
44
const PositiveNumber = t.refinement(
45
t.number,
46
(n): n is number => n > 0,
47
'PositiveNumber'
48
);
49
50
const result1 = PositiveNumber.decode(42);
51
// result: Right(42)
52
53
const result2 = PositiveNumber.decode(-5);
54
// result: Left([validation error])
55
56
// Create an email codec using string refinement
57
const Email = t.refinement(
58
t.string,
59
(s): s is string => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),
60
'Email'
61
);
62
63
const email = Email.decode("user@example.com");
64
// result: Right("user@example.com")
65
66
// Create non-empty array codec
67
const NonEmptyArray = <A>(codec: t.Type<A>) =>
68
t.refinement(
69
t.array(codec),
70
(arr): arr is [A, ...A[]] => arr.length > 0,
71
`NonEmptyArray<${codec.name}>`
72
);
73
74
const NonEmptyStrings = NonEmptyArray(t.string);
75
const result3 = NonEmptyStrings.decode(["hello"]);
76
// result: Right(["hello"])
77
78
const result4 = NonEmptyStrings.decode([]);
79
// result: Left([validation error])
80
```
81
82
### Brand Function
83
84
Create branded types that are distinct at the type level while sharing runtime representation.
85
86
```typescript { .api }
87
/**
88
* Create a branded codec that adds a compile-time brand to values
89
* @param codec - The base codec to brand
90
* @param predicate - Refinement predicate for the brand
91
* @param name - Brand name (becomes part of the type)
92
*/
93
function brand<C extends Any, N extends string, B extends { readonly [K in N]: symbol }>(
94
codec: C,
95
predicate: Refinement<TypeOf<C>, Branded<TypeOf<C>, B>>,
96
name: N
97
): BrandC<C, B>;
98
99
interface BrandC<C extends Any, B> extends RefinementType<C, Branded<TypeOf<C>, B>, OutputOf<C>, InputOf<C>> {}
100
101
/** Brand interface for creating branded types */
102
interface Brand<B> {
103
readonly [_brand]: B;
104
}
105
106
/** Branded type combining base type with brand */
107
type Branded<A, B> = A & Brand<B>;
108
```
109
110
**Usage Examples:**
111
112
```typescript
113
import * as t from "io-ts";
114
115
// Create a UserId brand
116
interface UserIdBrand {
117
readonly UserId: unique symbol;
118
}
119
120
const UserId = t.brand(
121
t.number,
122
(n): n is t.Branded<number, UserIdBrand> => Number.isInteger(n) && n > 0,
123
'UserId'
124
);
125
126
type UserId = t.TypeOf<typeof UserId>;
127
// UserId = number & Brand<UserIdBrand>
128
129
const userId = UserId.decode(123);
130
// result: Right(123) with UserId brand
131
132
// Create an Email brand
133
interface EmailBrand {
134
readonly Email: unique symbol;
135
}
136
137
const EmailBranded = t.brand(
138
t.string,
139
(s): s is t.Branded<string, EmailBrand> => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s),
140
'Email'
141
);
142
143
type Email = t.TypeOf<typeof EmailBranded>;
144
// Email = string & Brand<EmailBrand>
145
146
// Branded types prevent accidental mixing
147
function sendEmail(to: Email, from: UserId) {
148
// Implementation
149
}
150
151
const email = EmailBranded.decode("user@example.com");
152
const id = UserId.decode(456);
153
154
if (email._tag === "Right" && id._tag === "Right") {
155
sendEmail(email.right, id.right); // Type safe
156
// sendEmail(id.right, email.right); // Type error!
157
}
158
```
159
160
### Built-in Integer Brand
161
162
Pre-defined integer codec using the branding system.
163
164
```typescript { .api }
165
/** Integer brand interface */
166
interface IntBrand {
167
readonly Int: unique symbol;
168
}
169
170
/** Branded integer type */
171
type Int = Branded<number, IntBrand>;
172
173
/** Integer codec that validates integer numbers */
174
const Int: BrandC<NumberC, IntBrand>;
175
176
/** Pre-defined Integer codec (deprecated, use Int) */
177
const Integer: RefinementC<NumberC>;
178
```
179
180
**Usage Example:**
181
182
```typescript
183
import * as t from "io-ts";
184
185
const result1 = t.Int.decode(42);
186
// result: Right(42) with Int brand
187
188
const result2 = t.Int.decode(42.5);
189
// result: Left([validation error - not an integer])
190
191
const result3 = t.Int.decode("42");
192
// result: Left([validation error - not a number])
193
194
// Type-level distinction
195
function processInt(value: t.Int) {
196
// value is guaranteed to be an integer
197
return value * 2;
198
}
199
200
if (result1._tag === "Right") {
201
const doubled = processInt(result1.right); // Type safe
202
}
203
```
204
205
## Advanced Usage Patterns
206
207
### Multiple Refinements
208
209
```typescript
210
import * as t from "io-ts";
211
212
// Chain multiple refinements
213
const PositiveEvenNumber = t.refinement(
214
t.refinement(
215
t.number,
216
(n): n is number => n > 0,
217
'PositiveNumber'
218
),
219
(n): n is number => n % 2 === 0,
220
'EvenNumber'
221
);
222
223
const result = PositiveEvenNumber.decode(10);
224
// result: Right(10) - positive and even
225
226
const invalid1 = PositiveEvenNumber.decode(-2);
227
// result: Left([validation error - not positive])
228
229
const invalid2 = PositiveEvenNumber.decode(3);
230
// result: Left([validation error - not even])
231
```
232
233
### Custom Branded Types
234
235
```typescript
236
import * as t from "io-ts";
237
238
// Create a URL brand
239
interface UrlBrand {
240
readonly Url: unique symbol;
241
}
242
243
const Url = t.brand(
244
t.string,
245
(s): s is t.Branded<string, UrlBrand> => {
246
try {
247
new URL(s);
248
return true;
249
} catch {
250
return false;
251
}
252
},
253
'Url'
254
);
255
256
type Url = t.TypeOf<typeof Url>;
257
258
// Create a non-empty string brand
259
interface NonEmptyStringBrand {
260
readonly NonEmptyString: unique symbol;
261
}
262
263
const NonEmptyString = t.brand(
264
t.string,
265
(s): s is t.Branded<string, NonEmptyStringBrand> => s.length > 0,
266
'NonEmptyString'
267
);
268
269
type NonEmptyString = t.TypeOf<typeof NonEmptyString>;
270
271
// Use branded types in complex structures
272
const UserProfile = t.type({
273
name: NonEmptyString,
274
website: Url,
275
id: t.Int
276
});
277
278
type UserProfile = t.TypeOf<typeof UserProfile>;
279
```
280
281
### Range-based Refinements
282
283
```typescript
284
import * as t from "io-ts";
285
286
// Create range validation
287
const range = (min: number, max: number) =>
288
t.refinement(
289
t.number,
290
(n): n is number => n >= min && n <= max,
291
`Range(${min}, ${max})`
292
);
293
294
const Age = range(0, 150);
295
const Percentage = range(0, 100);
296
const Temperature = range(-273.15, Infinity);
297
298
const User = t.type({
299
name: t.string,
300
age: Age,
301
score: Percentage
302
});
303
304
const result = User.decode({
305
name: "Alice",
306
age: 25,
307
score: 95
308
});
309
// result: Right({ name: "Alice", age: 25, score: 95 })
310
311
const invalid = User.decode({
312
name: "Bob",
313
age: 200, // Too old
314
score: 110 // Over 100%
315
});
316
// result: Left([validation errors for age and score])
317
```
318
319
### String Pattern Refinements
320
321
```typescript
322
import * as t from "io-ts";
323
324
// Create pattern-based string codecs
325
const pattern = (regex: RegExp, name: string) =>
326
t.refinement(
327
t.string,
328
(s): s is string => regex.test(s),
329
name
330
);
331
332
const PhoneNumber = pattern(/^\+?[\d\s()-]+$/, 'PhoneNumber');
333
const PostalCode = pattern(/^\d{5}(-\d{4})?$/, 'PostalCode');
334
const HexColor = pattern(/^#[0-9A-Fa-f]{6}$/, 'HexColor');
335
336
const ContactInfo = t.type({
337
phone: PhoneNumber,
338
zip: PostalCode,
339
favoriteColor: HexColor
340
});
341
342
const result = ContactInfo.decode({
343
phone: "+1 (555) 123-4567",
344
zip: "12345",
345
favoriteColor: "#FF0000"
346
});
347
// result: Right(contact info)
348
```
349
350
### Combining Brands and Refinements
351
352
```typescript
353
import * as t from "io-ts";
354
355
// Create a sophisticated ID system
356
interface ProductIdBrand {
357
readonly ProductId: unique symbol;
358
}
359
360
interface CategoryIdBrand {
361
readonly CategoryId: unique symbol;
362
}
363
364
const ProductId = t.brand(
365
t.refinement(
366
t.string,
367
(s): s is string => s.startsWith('PROD-') && s.length === 10,
368
'ProductIdFormat'
369
),
370
(s): s is t.Branded<string, ProductIdBrand> => true,
371
'ProductId'
372
);
373
374
const CategoryId = t.brand(
375
t.refinement(
376
t.string,
377
(s): s is string => s.startsWith('CAT-') && s.length === 9,
378
'CategoryIdFormat'
379
),
380
(s): s is t.Branded<string, CategoryIdBrand> => true,
381
'CategoryId'
382
);
383
384
type ProductId = t.TypeOf<typeof ProductId>;
385
type CategoryId = t.TypeOf<typeof CategoryId>;
386
387
// These types are now completely distinct
388
function assignCategory(productId: ProductId, categoryId: CategoryId) {
389
// Implementation
390
}
391
392
const prodId = ProductId.decode("PROD-12345");
393
const catId = CategoryId.decode("CAT-54321");
394
395
if (prodId._tag === "Right" && catId._tag === "Right") {
396
assignCategory(prodId.right, catId.right); // Type safe
397
// assignCategory(catId.right, prodId.right); // Type error!
398
}
399
```