0
# Collection Codecs
1
2
Codecs for working with arrays, sets, and maps including non-empty arrays and proper serialization of complex collection types. These codecs enable type-safe handling of collections with specialized constraints and serialization formats.
3
4
## Capabilities
5
6
### NonEmptyArray
7
8
Creates codecs for fp-ts NonEmptyArray types that ensure arrays always contain at least one element.
9
10
```typescript { .api }
11
/**
12
* Type interface for NonEmptyArray codec
13
*/
14
interface NonEmptyArrayC<C extends t.Mixed>
15
extends t.Type<NonEmptyArray<t.TypeOf<C>>, NonEmptyArray<t.OutputOf<C>>, unknown> {}
16
17
/**
18
* Creates a codec for NonEmptyArray<A> that validates arrays are non-empty
19
* @param codec - The codec for individual array elements
20
* @param name - Optional name for the codec
21
* @returns NonEmptyArray codec that ensures at least one element
22
*/
23
function nonEmptyArray<C extends t.Mixed>(
24
codec: C,
25
name?: string
26
): NonEmptyArrayC<C>;
27
```
28
29
**Usage Examples:**
30
31
```typescript
32
import { nonEmptyArray } from "io-ts-types";
33
import * as t from "io-ts";
34
import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray";
35
36
// Create codec for non-empty array of strings
37
const NonEmptyStringArray = nonEmptyArray(t.string);
38
39
const result1 = NonEmptyStringArray.decode(["hello", "world"]);
40
// Right(NonEmptyArray<string>)
41
42
const result2 = NonEmptyStringArray.decode(["single"]);
43
// Right(NonEmptyArray<string>)
44
45
const result3 = NonEmptyStringArray.decode([]);
46
// Left([ValidationError]) - empty array not allowed
47
48
const result4 = NonEmptyStringArray.decode(["hello", 123]);
49
// Left([ValidationError]) - invalid element type
50
51
// Encoding back to regular array
52
const nonEmptyArray = result1.right;
53
const encoded = NonEmptyStringArray.encode(nonEmptyArray);
54
// ["hello", "world"]
55
```
56
57
### ReadonlyNonEmptyArray
58
59
Creates codecs for readonly non-empty arrays, providing immutable collection guarantees.
60
61
```typescript { .api }
62
/**
63
* Interface for readonly non-empty arrays
64
*/
65
interface ReadonlyNonEmptyArray<A> extends ReadonlyArray<A> {
66
readonly 0: A;
67
}
68
69
/**
70
* Type interface for ReadonlyNonEmptyArray codec
71
*/
72
interface ReadonlyNonEmptyArrayC<C extends t.Mixed>
73
extends t.Type<ReadonlyNonEmptyArray<t.TypeOf<C>>, ReadonlyNonEmptyArray<t.OutputOf<C>>, unknown> {}
74
75
/**
76
* Creates a codec for ReadonlyNonEmptyArray<A>
77
* @param codec - The codec for individual array elements
78
* @param name - Optional name for the codec
79
* @returns ReadonlyNonEmptyArray codec
80
*/
81
function readonlyNonEmptyArray<C extends t.Mixed>(
82
codec: C,
83
name?: string
84
): ReadonlyNonEmptyArrayC<C>;
85
```
86
87
**Usage Examples:**
88
89
```typescript
90
import { readonlyNonEmptyArray } from "io-ts-types";
91
import * as t from "io-ts";
92
93
const ReadonlyNumbers = readonlyNonEmptyArray(t.number);
94
95
const result1 = ReadonlyNumbers.decode([1, 2, 3]);
96
// Right(ReadonlyNonEmptyArray<number>)
97
98
const result2 = ReadonlyNumbers.decode([]);
99
// Left([ValidationError]) - empty array
100
101
// The result is readonly - no mutation methods available
102
if (result1._tag === "Right") {
103
const numbers = result1.right;
104
const first = numbers[0]; // 1 (guaranteed to exist)
105
// numbers.push(4); // TypeScript error - readonly array
106
}
107
```
108
109
### Set from Array
110
111
Creates codecs for Set types that serialize as arrays, with uniqueness validation and ordering support.
112
113
```typescript { .api }
114
/**
115
* Type interface for Set codec
116
*/
117
interface SetFromArrayC<C extends t.Mixed>
118
extends t.Type<Set<t.TypeOf<C>>, Array<t.OutputOf<C>>, unknown> {}
119
120
/**
121
* Creates a codec for Set<A> that serializes as an array
122
* @param codec - The codec for individual set elements
123
* @param O - Ord instance for element comparison and ordering
124
* @param name - Optional name for the codec
125
* @returns Set codec that validates uniqueness and requires Ord instance
126
*/
127
function setFromArray<C extends t.Mixed>(
128
codec: C,
129
O: Ord<t.TypeOf<C>>,
130
name?: string
131
): SetFromArrayC<C>;
132
```
133
134
**Usage Examples:**
135
136
```typescript
137
import { setFromArray } from "io-ts-types";
138
import * as t from "io-ts";
139
import { Ord } from "fp-ts/lib/Ord";
140
import * as S from "fp-ts/lib/string";
141
import * as N from "fp-ts/lib/number";
142
143
// String set with string ordering
144
const StringSet = setFromArray(t.string, S.Ord);
145
146
const result1 = StringSet.decode(["apple", "banana", "cherry"]);
147
// Right(Set<string>)
148
149
const result2 = StringSet.decode(["apple", "banana", "apple"]);
150
// Right(Set<string>) - duplicates removed
151
152
const result3 = StringSet.decode([]);
153
// Right(Set<string>) - empty set is valid
154
155
// Number set with number ordering
156
const NumberSet = setFromArray(t.number, N.Ord);
157
158
const result4 = NumberSet.decode([3, 1, 4, 1, 5]);
159
// Right(Set<number>) - duplicates removed, order determined by Ord
160
161
// Encoding back to array (ordered by Ord)
162
const stringSet = new Set(["zebra", "apple", "banana"]);
163
const encoded = StringSet.encode(stringSet);
164
// ["apple", "banana", "zebra"] - sorted by string Ord
165
```
166
167
### ReadonlySet from Array
168
169
Creates codecs for ReadonlySet types with immutable guarantees.
170
171
```typescript { .api }
172
/**
173
* Type interface for ReadonlySet codec
174
*/
175
interface ReadonlySetFromArrayC<C extends t.Mixed>
176
extends t.Type<ReadonlySet<t.TypeOf<C>>, ReadonlyArray<t.OutputOf<C>>, unknown> {}
177
178
/**
179
* Creates a codec for ReadonlySet<A>
180
* @param codec - The codec for individual set elements
181
* @param O - Ord instance for element comparison and ordering
182
* @param name - Optional name for the codec
183
* @returns ReadonlySet codec
184
*/
185
function readonlySetFromArray<C extends t.Mixed>(
186
codec: C,
187
O: Ord<t.TypeOf<C>>,
188
name?: string
189
): ReadonlySetFromArrayC<C>;
190
```
191
192
### Map from Entries
193
194
Creates codecs for Map types that serialize as arrays of key-value pairs.
195
196
```typescript { .api }
197
/**
198
* Type interface for Map codec
199
*/
200
interface MapFromEntriesC<K extends t.Mixed, V extends t.Mixed>
201
extends t.Type<Map<t.TypeOf<K>, t.TypeOf<V>>, Array<[t.OutputOf<K>, t.OutputOf<V>]>, unknown> {}
202
203
/**
204
* Creates a codec for Map<K, V> that serializes as an array of key-value pairs
205
* @param keyCodec - The codec for map keys
206
* @param KO - Ord instance for key comparison and ordering
207
* @param valueCodec - The codec for map values
208
* @param name - Optional name for the codec
209
* @returns Map codec that requires Ord instance for keys
210
*/
211
function mapFromEntries<K extends t.Mixed, V extends t.Mixed>(
212
keyCodec: K,
213
KO: Ord<t.TypeOf<K>>,
214
valueCodec: V,
215
name?: string
216
): MapFromEntriesC<K, V>;
217
```
218
219
**Usage Examples:**
220
221
```typescript
222
import { mapFromEntries } from "io-ts-types";
223
import * as t from "io-ts";
224
import * as S from "fp-ts/lib/string";
225
import * as N from "fp-ts/lib/number";
226
227
// Map with string keys and number values
228
const StringNumberMap = mapFromEntries(t.string, S.Ord, t.number);
229
230
const result1 = StringNumberMap.decode([
231
["apple", 5],
232
["banana", 3],
233
["cherry", 8]
234
]);
235
// Right(Map<string, number>)
236
237
const result2 = StringNumberMap.decode([]);
238
// Right(Map<string, number>) - empty map is valid
239
240
const result3 = StringNumberMap.decode([
241
["apple", 5],
242
["apple", 10] // Duplicate key - last value wins
243
]);
244
// Right(Map<string, number>) - Map with apple -> 10
245
246
// Invalid value type
247
const result4 = StringNumberMap.decode([
248
["apple", "not-a-number"]
249
]);
250
// Left([ValidationError])
251
252
// Encoding back to entries array
253
const map = new Map([["zebra", 1], ["apple", 2], ["banana", 3]]);
254
const encoded = StringNumberMap.encode(map);
255
// [["apple", 2], ["banana", 3], ["zebra", 1]] - sorted by key Ord
256
```
257
258
### ReadonlyMap from Entries
259
260
Creates codecs for ReadonlyMap types with immutable guarantees.
261
262
```typescript { .api }
263
/**
264
* Type interface for ReadonlyMap codec
265
*/
266
interface ReadonlyMapFromEntriesC<K extends t.Mixed, V extends t.Mixed>
267
extends t.Type<ReadonlyMap<t.TypeOf<K>, t.TypeOf<V>>, ReadonlyArray<[t.OutputOf<K>, t.OutputOf<V>]>, unknown> {}
268
269
/**
270
* Creates a codec for ReadonlyMap<K, V>
271
* @param keyCodec - The codec for map keys
272
* @param KO - Ord instance for key comparison and ordering
273
* @param valueCodec - The codec for map values
274
* @param name - Optional name for the codec
275
* @returns ReadonlyMap codec
276
*/
277
function readonlyMapFromEntries<K extends t.Mixed, V extends t.Mixed>(
278
keyCodec: K,
279
KO: Ord<t.TypeOf<K>>,
280
valueCodec: V,
281
name?: string
282
): ReadonlyMapFromEntriesC<K, V>;
283
```
284
285
## Common Usage Patterns
286
287
### API Response with Collections
288
289
```typescript
290
import * as t from "io-ts";
291
import { nonEmptyArray, setFromArray, mapFromEntries } from "io-ts-types";
292
import * as S from "fp-ts/lib/string";
293
import * as N from "fp-ts/lib/number";
294
295
const User = t.type({
296
id: t.number,
297
name: t.string,
298
email: t.string
299
});
300
301
const Project = t.type({
302
id: t.number,
303
name: t.string,
304
members: nonEmptyArray(User), // At least one member required
305
tags: setFromArray(t.string, S.Ord), // Unique tags
306
metadata: mapFromEntries(t.string, S.Ord, t.string) // Key-value metadata
307
});
308
309
const projectData = {
310
id: 1,
311
name: "Web Application",
312
members: [
313
{ id: 1, name: "Alice", email: "alice@example.com" },
314
{ id: 2, name: "Bob", email: "bob@example.com" }
315
],
316
tags: ["typescript", "react", "nodejs", "react"], // Duplicate removed
317
metadata: [
318
["repository", "https://github.com/org/project"],
319
["status", "active"],
320
["priority", "high"]
321
]
322
};
323
324
const parsed = Project.decode(projectData);
325
// Right({
326
// id: 1,
327
// name: "Web Application",
328
// members: NonEmptyArray<User>,
329
// tags: Set<string>,
330
// metadata: Map<string, string>
331
// })
332
```
333
334
### Configuration with Collections
335
336
```typescript
337
import * as t from "io-ts";
338
import { setFromArray, mapFromEntries, nonEmptyArray } from "io-ts-types";
339
import * as S from "fp-ts/lib/string";
340
import * as N from "fp-ts/lib/number";
341
342
const ServerConfig = t.type({
343
hosts: nonEmptyArray(t.string), // At least one host
344
allowedOrigins: setFromArray(t.string, S.Ord), // Unique origins
345
ports: setFromArray(t.number, N.Ord), // Unique ports
346
environment: mapFromEntries(t.string, S.Ord, t.string) // Env variables
347
});
348
349
const config = {
350
hosts: ["api.example.com", "api2.example.com"],
351
allowedOrigins: ["https://app.example.com", "https://admin.example.com"],
352
ports: [8080, 8443, 9000],
353
environment: [
354
["NODE_ENV", "production"],
355
["LOG_LEVEL", "info"],
356
["DATABASE_URL", "postgresql://..."]
357
]
358
};
359
360
const validated = ServerConfig.decode(config);
361
// Right({ hosts: NonEmptyArray, allowedOrigins: Set, ports: Set, environment: Map })
362
```
363
364
### Data Processing Pipeline
365
366
```typescript
367
import * as t from "io-ts";
368
import { nonEmptyArray, setFromArray } from "io-ts-types";
369
import * as S from "fp-ts/lib/string";
370
import { pipe } from "fp-ts/lib/function";
371
import * as NEA from "fp-ts/lib/NonEmptyArray";
372
373
const ProcessingJob = t.type({
374
id: t.string,
375
inputFiles: nonEmptyArray(t.string), // Must have input files
376
outputFormats: setFromArray(t.string, S.Ord), // Unique output formats
377
steps: nonEmptyArray(t.string) // Must have processing steps
378
});
379
380
const jobData = {
381
id: "job_001",
382
inputFiles: ["data.csv", "metadata.json"],
383
outputFormats: ["json", "csv", "xml", "json"], // Duplicate removed
384
steps: ["validate", "transform", "aggregate", "export"]
385
};
386
387
const job = ProcessingJob.decode(jobData);
388
389
if (job._tag === "Right") {
390
const { inputFiles, outputFormats, steps } = job.right;
391
392
// Safe operations on non-empty arrays
393
const firstFile = NEA.head(inputFiles); // "data.csv"
394
const totalSteps = steps.length; // 4
395
396
// Safe operations on sets
397
const hasJsonOutput = outputFormats.has("json"); // true
398
const formatCount = outputFormats.size; // 3 (duplicates removed)
399
400
console.log(`Processing ${totalSteps} steps for ${inputFiles.length} files`);
401
console.log(`Generating ${formatCount} output formats`);
402
}
403
```
404
405
### Database Entity with Collections
406
407
```typescript
408
import * as t from "io-ts";
409
import { nonEmptyArray, setFromArray, mapFromEntries } from "io-ts-types";
410
import * as S from "fp-ts/lib/string";
411
import * as N from "fp-ts/lib/number";
412
413
const Category = t.type({
414
id: t.number,
415
name: t.string
416
});
417
418
const Article = t.type({
419
id: t.number,
420
title: t.string,
421
content: t.string,
422
categories: nonEmptyArray(Category), // Must be in at least one category
423
tags: setFromArray(t.string, S.Ord), // Unique tags
424
metadata: mapFromEntries(t.string, S.Ord, t.string), // Custom metadata
425
relatedArticles: setFromArray(t.number, N.Ord) // Unique article IDs
426
});
427
428
const articleData = {
429
id: 1,
430
title: "Getting Started with TypeScript",
431
content: "TypeScript is a typed superset of JavaScript...",
432
categories: [
433
{ id: 1, name: "Programming" },
434
{ id: 2, name: "TypeScript" }
435
],
436
tags: ["typescript", "javascript", "tutorial", "beginners"],
437
metadata: [
438
["author", "Alice Developer"],
439
["publishDate", "2023-12-25"],
440
["readTime", "10 minutes"]
441
],
442
relatedArticles: [5, 12, 8, 15]
443
};
444
445
const parsed = Article.decode(articleData);
446
// Right({
447
// id: 1,
448
// title: "...",
449
// content: "...",
450
// categories: NonEmptyArray<Category>,
451
// tags: Set<string>,
452
// metadata: Map<string, string>,
453
// relatedArticles: Set<number>
454
// })
455
```
456
457
### Form Processing with Collections
458
459
```typescript
460
import * as t from "io-ts";
461
import { nonEmptyArray, setFromArray } from "io-ts-types";
462
import * as S from "fp-ts/lib/string";
463
464
const SurveyResponse = t.type({
465
respondentId: t.string,
466
answers: nonEmptyArray(t.string), // Must answer at least one question
467
selectedOptions: setFromArray(t.string, S.Ord), // Unique selected options
468
feedback: t.union([t.string, t.null])
469
});
470
471
// Form data with repeated selections
472
const responseData = {
473
respondentId: "user_123",
474
answers: ["Satisfied", "Very Good", "Yes"],
475
selectedOptions: ["option_a", "option_c", "option_b", "option_a"], // Duplicates
476
feedback: "Great survey!"
477
};
478
479
const validated = SurveyResponse.decode(responseData);
480
481
if (validated._tag === "Right") {
482
const response = validated.right;
483
484
// selectedOptions is now a Set with duplicates removed
485
const uniqueSelections = Array.from(response.selectedOptions);
486
// ["option_a", "option_b", "option_c"] - sorted by string Ord
487
488
console.log(`Respondent ${response.respondentId} provided ${response.answers.length} answers`);
489
console.log(`Selected ${response.selectedOptions.size} unique options`);
490
}
491
```