-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcreateDefaultCodec.ts
More file actions
139 lines (128 loc) · 4.26 KB
/
createDefaultCodec.ts
File metadata and controls
139 lines (128 loc) · 4.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as t from 'io-ts';
/**
* Creates a default value for an io-ts codec.
* @param codec - the codec whose default we want to create
* @returns an object honoring the io-ts codec
*
* Use Case: CSV Generation from Typed Objects
* Consider the following codec:
*
* const UserCodec = t.intersection([
* t.type({
* id: t.string,
* name: t.string,
* }),
* t.partial({
* email: t.string,
* })
* ])
*
* Suppose we want to generate a CSV file from the objects below with UserCodec type:
*
* const users = [
* { id: '1', name: 'Alice', email: 'alice@email.com' },
* { id: '2', name: 'Bob' },
* ]
*
* To generate a CSV, we need a consistent set of columns (headers) that includes
* ALL possible fields, even if some objects don't have all properties. In the example
* above, though, notice that the second user does not have the email property.
*
* In such a case, createDefaultCodec(UserCodec) is useful to construct an object
* with all possible fields, which can then be used to generate the complete CSV header:
* "id,name,email".
*
* The example above is pretty simple, but things get very compliated the more complex
* is the codec. In any case, createDefaultCodec is useful for outputting the expected CSV header.
*/
export const createDefaultCodec = <C extends t.Mixed>(
codec: C,
): t.TypeOf<C> => {
// If the codec is an union
if (codec instanceof t.UnionType) {
// The default for a union containing arrays is a default array
const arrayType = codec.types.find(
(type: any) => type instanceof t.ArrayType,
);
if (arrayType) {
return createDefaultCodec(arrayType);
}
// If the union does not have an array, but have objects, the default is the object
const objectType = codec.types.find(
(type: any) =>
type instanceof t.InterfaceType ||
type instanceof t.PartialType ||
type instanceof t.IntersectionType ||
type instanceof t.ArrayType,
);
if (objectType) {
return createDefaultCodec(objectType);
}
// Otherwise, null is next in priority
const hasNull = codec.types.some(
(type: any) => type instanceof t.NullType || type.name === 'null',
);
if (hasNull) {
return null as t.TypeOf<C>;
}
// If no null type found, default to first type
return createDefaultCodec(codec.types[0]);
}
// The default of an interface or partial type, is an object with that interface or partial type
if (codec instanceof t.InterfaceType || codec instanceof t.PartialType) {
const defaults: Record<string, any> = {};
Object.entries(codec.props).forEach(([key, type]) => {
defaults[key] = createDefaultCodec(type as any);
});
return defaults as t.TypeOf<C>;
}
// The default of an intersection, is the merged defaults of the intersection values
if (codec instanceof t.IntersectionType) {
return codec.types.reduce(
(acc: t.TypeOf<C>, type: any) => ({
...acc,
...createDefaultCodec(type),
}),
{},
);
}
/**
* The default of an Array is an empty array, unless it is an array of objects, in which
* case the default is an array of one default object.
*/
if (codec instanceof t.ArrayType) {
const elementType = codec.type;
const isObjectType =
elementType instanceof t.InterfaceType ||
elementType instanceof t.PartialType ||
elementType instanceof t.IntersectionType;
return (
isObjectType ? [createDefaultCodec(elementType)] : []
) as t.TypeOf<C>;
}
// The default of a literal type is its value
if (codec instanceof t.LiteralType) {
return codec.value as t.TypeOf<C>;
}
// The default of an object type is an empty object
if (codec instanceof t.ObjectType || codec instanceof t.UnknownType) {
return {} as t.TypeOf<C>;
}
// Handle primitive and common types
switch (codec.name) {
case 'string':
return '' as t.TypeOf<C>;
case 'number':
return 0 as t.TypeOf<C>;
case 'boolean':
return false as t.TypeOf<C>;
case 'null':
return null as t.TypeOf<C>;
case 'undefined':
return undefined as t.TypeOf<C>;
default:
return null as t.TypeOf<C>;
}
};