0
# Functional Programming Types
1
2
Codecs for fp-ts Option and Either types with JSON serialization support, enabling functional programming patterns in API communication. These codecs allow safe serialization and deserialization of functional programming constructs to and from JSON representations.
3
4
## Capabilities
5
6
### Option Type Codec
7
8
Creates codecs for fp-ts Option types with JSON serialization, allowing None and Some values to be transmitted over networks and stored in databases.
9
10
```typescript { .api }
11
/**
12
* Output type for None values in Option serialization
13
*/
14
type NoneOutput = { _tag: 'None' };
15
16
/**
17
* Output type for Some values in Option serialization
18
*/
19
type SomeOutput<A> = { _tag: 'Some'; value: A };
20
21
/**
22
* Union type for Option serialization output
23
*/
24
type OptionOutput<A> = NoneOutput | SomeOutput<A>;
25
26
/**
27
* Type interface for Option codec
28
*/
29
interface OptionC<C extends t.Mixed>
30
extends t.Type<Option<t.TypeOf<C>>, OptionOutput<t.OutputOf<C>>, unknown> {}
31
32
/**
33
* Creates a codec for Option<A> that can serialize/deserialize JSON representations
34
* @param codec - The inner codec for the Some value type
35
* @param name - Optional name for the codec
36
* @returns Option codec with JSON serialization support
37
*/
38
function option<C extends t.Mixed>(codec: C, name?: string): OptionC<C>;
39
```
40
41
**Usage Examples:**
42
43
```typescript
44
import { option } from "io-ts-types";
45
import * as t from "io-ts";
46
import * as O from "fp-ts/lib/Option";
47
48
// Create codec for optional string
49
const OptionalString = option(t.string);
50
51
// Decode Some value
52
const someResult = OptionalString.decode({ _tag: "Some", value: "hello" });
53
// Right(some("hello"))
54
55
// Decode None value
56
const noneResult = OptionalString.decode({ _tag: "None" });
57
// Right(none)
58
59
// Invalid structure
60
const invalidResult = OptionalString.decode({ _tag: "Invalid" });
61
// Left([ValidationError])
62
63
// Encoding Some to JSON
64
const someValue = O.some("world");
65
const encodedSome = OptionalString.encode(someValue);
66
// { _tag: "Some", value: "world" }
67
68
// Encoding None to JSON
69
const noneValue = O.none;
70
const encodedNone = OptionalString.encode(noneValue);
71
// { _tag: "None" }
72
```
73
74
### Option From Nullable
75
76
Creates Option codecs that treat null/undefined as None and other values as Some, providing a bridge between nullable values and Option types.
77
78
```typescript { .api }
79
/**
80
* Type interface for OptionFromNullable codec
81
*/
82
interface OptionFromNullableC<C extends t.Mixed>
83
extends t.Type<Option<t.TypeOf<C>>, t.OutputOf<C> | null, unknown> {}
84
85
/**
86
* Creates a codec for Option<A> that treats null/undefined as None
87
* @param codec - The inner codec for the Some value type
88
* @param name - Optional name for the codec
89
* @returns Option codec that handles nullable inputs
90
*/
91
function optionFromNullable<C extends t.Mixed>(
92
codec: C,
93
name?: string
94
): OptionFromNullableC<C>;
95
```
96
97
**Usage Examples:**
98
99
```typescript
100
import { optionFromNullable } from "io-ts-types";
101
import * as t from "io-ts";
102
import * as O from "fp-ts/lib/Option";
103
104
const OptionalStringFromNull = optionFromNullable(t.string);
105
106
// Decode regular value as Some
107
const result1 = OptionalStringFromNull.decode("hello");
108
// Right(some("hello"))
109
110
// Decode null as None
111
const result2 = OptionalStringFromNull.decode(null);
112
// Right(none)
113
114
// Decode undefined as None
115
const result3 = OptionalStringFromNull.decode(undefined);
116
// Right(none)
117
118
// Invalid inner type
119
const result4 = OptionalStringFromNull.decode(123);
120
// Left([ValidationError]) - number is not a string
121
122
// Encoding Some back to regular value
123
const someValue = O.some("world");
124
const encoded1 = OptionalStringFromNull.encode(someValue);
125
// "world"
126
127
// Encoding None back to null
128
const noneValue = O.none;
129
const encoded2 = OptionalStringFromNull.encode(noneValue);
130
// null
131
```
132
133
### Either Type Codec
134
135
Creates codecs for fp-ts Either types with JSON serialization, allowing Left and Right values to be transmitted and stored.
136
137
```typescript { .api }
138
/**
139
* Output type for Left values in Either serialization
140
*/
141
type LeftOutput<L> = { _tag: 'Left'; left: L };
142
143
/**
144
* Output type for Right values in Either serialization
145
*/
146
type RightOutput<R> = { _tag: 'Right'; right: R };
147
148
/**
149
* Union type for Either serialization output
150
*/
151
type EitherOutput<L, R> = LeftOutput<L> | RightOutput<R>;
152
153
/**
154
* Type interface for Either codec
155
*/
156
interface EitherC<L extends t.Mixed, R extends t.Mixed>
157
extends t.Type<Either<t.TypeOf<L>, t.TypeOf<R>>, EitherOutput<t.OutputOf<L>, t.OutputOf<R>>, unknown> {}
158
159
/**
160
* Creates a codec for Either<L, R> that can serialize/deserialize JSON representations
161
* @param leftCodec - Codec for the Left value type
162
* @param rightCodec - Codec for the Right value type
163
* @param name - Optional name for the codec
164
* @returns Either codec with JSON serialization support
165
*/
166
function either<L extends t.Mixed, R extends t.Mixed>(
167
leftCodec: L,
168
rightCodec: R,
169
name?: string
170
): EitherC<L, R>;
171
```
172
173
**Usage Examples:**
174
175
```typescript
176
import { either } from "io-ts-types";
177
import * as t from "io-ts";
178
import * as E from "fp-ts/lib/Either";
179
180
// Create codec for Either<string, number>
181
const StringOrNumber = either(t.string, t.number);
182
183
// Decode Right value
184
const rightResult = StringOrNumber.decode({ _tag: "Right", right: 42 });
185
// Right(right(42))
186
187
// Decode Left value
188
const leftResult = StringOrNumber.decode({ _tag: "Left", left: "error" });
189
// Right(left("error"))
190
191
// Invalid structure
192
const invalidResult = StringOrNumber.decode({ _tag: "Invalid" });
193
// Left([ValidationError])
194
195
// Invalid Right type
196
const invalidRight = StringOrNumber.decode({ _tag: "Right", right: true });
197
// Left([ValidationError]) - boolean is not a number
198
199
// Encoding Right to JSON
200
const rightValue = E.right(100);
201
const encodedRight = StringOrNumber.encode(rightValue);
202
// { _tag: "Right", right: 100 }
203
204
// Encoding Left to JSON
205
const leftValue = E.left("failure");
206
const encodedLeft = StringOrNumber.encode(leftValue);
207
// { _tag: "Left", left: "failure" }
208
```
209
210
## Common Usage Patterns
211
212
### API Error Handling
213
214
```typescript
215
import * as t from "io-ts";
216
import { either, option } from "io-ts-types";
217
import * as E from "fp-ts/lib/Either";
218
import * as O from "fp-ts/lib/Option";
219
220
const ApiError = t.type({
221
code: t.string,
222
message: t.string,
223
details: option(t.string)
224
});
225
226
const UserData = t.type({
227
id: t.number,
228
name: t.string,
229
email: t.string
230
});
231
232
const ApiResponse = either(ApiError, UserData);
233
234
// Success response
235
const successResponse = {
236
_tag: "Right",
237
right: {
238
id: 1,
239
name: "Alice",
240
email: "alice@example.com"
241
}
242
};
243
244
const parsedSuccess = ApiResponse.decode(successResponse);
245
// Right(right({ id: 1, name: "Alice", email: "alice@example.com" }))
246
247
// Error response
248
const errorResponse = {
249
_tag: "Left",
250
left: {
251
code: "USER_NOT_FOUND",
252
message: "User not found",
253
details: { _tag: "Some", value: "User ID 123 does not exist" }
254
}
255
};
256
257
const parsedError = ApiResponse.decode(errorResponse);
258
// Right(left({ code: "USER_NOT_FOUND", message: "User not found", details: some("...") }))
259
```
260
261
### Optional Configuration Fields
262
263
```typescript
264
import * as t from "io-ts";
265
import { option, optionFromNullable } from "io-ts-types";
266
import * as O from "fp-ts/lib/Option";
267
268
const DatabaseConfig = t.type({
269
host: t.string,
270
port: t.number,
271
ssl: optionFromNullable(t.boolean), // null becomes None
272
timeout: option(t.number), // Explicit Option structure
273
credentials: optionFromNullable(t.type({
274
username: t.string,
275
password: t.string
276
}))
277
});
278
279
// Configuration with mixed optional fields
280
const config1 = {
281
host: "localhost",
282
port: 5432,
283
ssl: true, // Some(true)
284
timeout: { _tag: "None" }, // Explicit None
285
credentials: { // Some(credentials)
286
username: "admin",
287
password: "secret"
288
}
289
};
290
291
const parsed1 = DatabaseConfig.decode(config1);
292
293
// Configuration with null values
294
const config2 = {
295
host: "remote.db.com",
296
port: 5432,
297
ssl: null, // None (from null)
298
timeout: { _tag: "Some", value: 30000 }, // Explicit Some
299
credentials: null // None (from null)
300
};
301
302
const parsed2 = DatabaseConfig.decode(config2);
303
```
304
305
### Form Validation with Optional Fields
306
307
```typescript
308
import * as t from "io-ts";
309
import { optionFromNullable } from "io-ts-types";
310
import { pipe } from "fp-ts/lib/function";
311
import * as O from "fp-ts/lib/Option";
312
313
const UserProfile = t.type({
314
username: t.string,
315
email: t.string,
316
bio: optionFromNullable(t.string),
317
website: optionFromNullable(t.string),
318
avatar: optionFromNullable(t.string)
319
});
320
321
const formData = {
322
username: "alice_dev",
323
email: "alice@example.com",
324
bio: "Full-stack developer", // Some("Full-stack developer")
325
website: null, // None
326
avatar: undefined // None
327
};
328
329
const validated = UserProfile.decode(formData);
330
331
if (validated._tag === "Right") {
332
const profile = validated.right;
333
334
// Safe handling of optional fields
335
const displayBio = pipe(
336
profile.bio,
337
O.getOrElse(() => "No bio provided")
338
);
339
340
const hasWebsite = O.isSome(profile.website);
341
342
console.log(`Bio: ${displayBio}`); // Bio: Full-stack developer
343
console.log(`Has website: ${hasWebsite}`); // Has website: false
344
}
345
```
346
347
### Result Type Pattern
348
349
```typescript
350
import * as t from "io-ts";
351
import { either } from "io-ts-types";
352
import * as E from "fp-ts/lib/Either";
353
import { pipe } from "fp-ts/lib/function";
354
355
const ValidationError = t.type({
356
field: t.string,
357
message: t.string
358
});
359
360
const ProcessingResult = either(
361
t.array(ValidationError), // Left: array of validation errors
362
t.type({ // Right: success result
363
id: t.string,
364
processed: t.boolean,
365
timestamp: t.number
366
})
367
);
368
369
// Success case
370
const successData = {
371
_tag: "Right",
372
right: {
373
id: "task_123",
374
processed: true,
375
timestamp: 1703505000000
376
}
377
};
378
379
// Error case
380
const errorData = {
381
_tag: "Left",
382
left: [
383
{ field: "email", message: "Invalid email format" },
384
{ field: "age", message: "Must be a positive number" }
385
]
386
};
387
388
function handleResult(data: unknown) {
389
return pipe(
390
ProcessingResult.decode(data),
391
E.fold(
392
(decodeErrors) => `Decode error: ${decodeErrors}`,
393
E.fold(
394
(validationErrors) => `Validation failed: ${validationErrors.map(e => e.message).join(", ")}`,
395
(success) => `Processed successfully: ${success.id}`
396
)
397
)
398
);
399
}
400
401
const result1 = handleResult(successData);
402
// "Processed successfully: task_123"
403
404
const result2 = handleResult(errorData);
405
// "Validation failed: Invalid email format, Must be a positive number"
406
```
407
408
### Database Query Results
409
410
```typescript
411
import * as t from "io-ts";
412
import { option, either } from "io-ts-types";
413
import * as O from "fp-ts/lib/Option";
414
import * as E from "fp-ts/lib/Either";
415
416
const User = t.type({
417
id: t.number,
418
name: t.string,
419
lastLogin: option(t.string) // Some(date) or None if never logged in
420
});
421
422
const DatabaseError = t.type({
423
code: t.string,
424
query: t.string
425
});
426
427
const QueryResult = either(DatabaseError, option(User));
428
429
// User found
430
const userFound = {
431
_tag: "Right",
432
right: {
433
_tag: "Some",
434
value: {
435
id: 1,
436
name: "Alice",
437
lastLogin: { _tag: "Some", value: "2023-12-25T10:30:00Z" }
438
}
439
}
440
};
441
442
// User not found (query succeeded, but no result)
443
const userNotFound = {
444
_tag: "Right",
445
right: { _tag: "None" }
446
};
447
448
// Database error
449
const dbError = {
450
_tag: "Left",
451
left: {
452
code: "CONNECTION_TIMEOUT",
453
query: "SELECT * FROM users WHERE id = 1"
454
}
455
};
456
457
function processQueryResult(result: unknown) {
458
return pipe(
459
QueryResult.decode(result),
460
E.fold(
461
(errors) => `Invalid result format: ${errors}`,
462
E.fold(
463
(dbErr) => `Database error ${dbErr.code}: ${dbErr.query}`,
464
O.fold(
465
() => "User not found",
466
(user) => `Found user: ${user.name} (ID: ${user.id})`
467
)
468
)
469
)
470
);
471
}
472
```