-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.d.ts
666 lines (640 loc) · 20.3 KB
/
index.d.ts
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
/**
* A library for constructing and running decoders that make sure your data
* looks as expected.
*
* This is a tool that is useful for bringing values safely into your code.
* The source of the data might be something like an HTTP API, user input or
* localStorage, where you can never be sure that you get what you expect.
*
* @packageDocumentation
*/
/**
* This is intended to be an opaque type.
* You really shouldn't be building `Decoder<T>` values on your own (because
* you can't, but TypeScript doesn't stop you). Use the exposed functions
* for that.
*
* If you ever feel like something is missing, please create an issue in the
* GitHub repository at https://github.com/schroffl/json-mapping
*
* @typeParam T - When running this decoder you get a value of this type
*/
export type Decoder<T> = {
/**
* @internal
*/
readonly __opaque_type: 'decoder'
/**
* @internal
*/
readonly __type: T
}
/**
* Mapped type that creates an object from the given type where every property
* is optional, but wraps the actual type in a Decoder. It's used by
* {@link Decode.object} and {@link Decode.instance}.
*
* @typeParam O - The type to generate the layout for
*
* @example
* If you had a User model like
* ```typescript
* class User {
* id: number;
* name: string;
* children: User[];
* }
* ```
*
* the mapped layout would look like this:
*
* ```typescript
* type UserLayout = {
* id?: Decoder<number>,
* name?: Decoder<string>,
* children?: Decoder<User[]>,
* }
* ```
*/
export type ObjectLayout<O> = {
[K in keyof O]?: Decoder<O[K]>
}
/**
* This namespace wraps all the decoders exposed by this package.
* It contains primitive decoders like {@link Decode.string} to more
* complicated ones like {@link Decode.map}.
* So basically all the building blocks you need for creating decoders for
* complex data structures.
*/
export namespace Decode {
/**
* Decode any valid JavaScript Number that is not NaN
*
* @example
* ```typescript
* decode(Decode.number, 42.2) == 42.2
* decode(Decode.number, 42) == 42
* decode(Decode.number, NaN) // Throws
* ```
*/
export const number: Decoder<number>
/**
* Decode any string value.
*
* @example
* ```typescript
* decode(Decode.string, 'abc') === 'abc'
* decode(Decode.string, 'my-string') === 'my-string'
* decode(Decode.string, 10) // Throws
* ```
*/
export const string: Decoder<string>
/**
* Decode an integer. Floating point values are not accepted.
*
* @example
* ```typescript
* decode(Decode.number, 42) == 42
* decode(Decode.number, 42.2) // Throws
* decode(Decode.number, NaN) // Throws
* ```
*/
export const integer: Decoder<number>
/**
* Decode either `true` or `false`. Nothing else.
*
* @example
* ```typescript
* decode(Decode.bool, true) === true
* decode(Decode.bool, false) === false
* decode(Decode.bool, undefined) // Throws
* ```
*/
export const bool: Decoder<boolean>
/**
* Decode the value as-is. You probably shouldn't use this, because there's
* a high chance you're abusing it as an escape-hatch.
* However, it has a valid use case for building custom decoders with the
* help of {@link Decode.andThen}. If you do that, please make sure that you keep
* everything safe.
*
* @example
* ```typescript
* decode(Decode.unknown, true) === true
* decode(Decode.unknown, undefined) === undefined
* decode(Decode.unknown, NaN)
*
* // This decoder really blindly passes on the value
* const symbol = Symbol('my-symbol');
* decode(Decode.unknown, symbol) === symbol
* ```
*/
export const unknown: Decoder<unknown>
/**
* Decode the value to an object. This implies nothing about the value that
* is being decoded, which can be anything.
* You could take a "plain" integer and "lift" it into an object.
*
* @example
* ```typescript
* const decoder = Decode.object({
* value: Decode.integer,
* });
*
* decode(decoder, 42) // { value: 42 }
* decode(decoder, 100) // { value: 100 }
* decode(decoder, '100') // Fails
* ```
*
* @param layout - Properties you want the object to have
*/
export function object<O>(layout: ObjectLayout<O>) : Decoder<O>
/**
* Decode an object with arbitrary keys and values of the same type T.
*
* @param child - Decoder to use for values
*
* @example
* ```typescript
* const raw = { en: 'Bread', fr: 'Pain', it: 'Pane' };
* const decoder = Decode.dict(Decode.string);
*
* decode(decoder, raw); // Works
*
* decode(decoder, { en: 128 }); // Fails
* ```
*/
export function dict<T>(child: Decoder<T>) : Decoder<{ [key: string]: T }>
/**
* This is mostly equivalent to {@link Decode.object}, but it creates an
* instance of the given class first. You should only use this for simple
* classes that don't have a complex constructor. Use `map` or `andThen`
* for complicated cases.
*
* @example
* If you had the following User model in your application
*
* ```typescript
* class User {
* id!: number;
* name!: string;
*
* get initials(): string {
* const parts = this.name.split(' ');
* return parts[0][0] + parts[1][0];
* }
* }
* ```
*
* your instance decoder would look like this.
*
* ```typescript
* const UserDecoder = Decode.instance(User, {
* id: Decode.field('id', Decode.integer),
* name: Decode.field('name', Decode.string),
* });
* ```
*
* Running this decoder on raw values will ensure that you
* always have an actual instance of your User class at hand.
*
* ```typescript
* const kobe = decode(UserDecoder, { id: 3, name: 'Kobe Bryant' });
* console.assert(kobe.id === 3)
* console.assert(kobe.name === 'Kobe Bryant')
* console.assert(kobe.initials === 'KB')
* ```
*
* @param ctor - The class you want to construct an instance of
* @param layout - Properties you want to set on the instance
*
* @see object
* @see ObjectLayout
*/
export function instance<O>(ctor: new () => O, layout: ObjectLayout<O>) : Decoder<O>
/**
* Access the given property of an object.
*
* @param name - Name of the property
* @param child - The decoder to run on the value of that property
*
* @example
* ```typescript
* const decoder = Decode.field('value', Decode.integer);
*
* decode(decoder, { value: 42 }) === 42
* decode(decoder, {}) // Fails
* decode(decoder, 42) // Fails
* ```
*/
export function field<T>(name: string, child: Decoder<T>) : Decoder<T>
/**
* Same as {@link Decode.field}, but it doesn't fail when the property is not
* present on the object. In that case, the provided value is returned.
*
* @param name - Name of the property
* @param value - The value to use if the field is absent
* @param child - The decoder to run on the value of that property
*
* @example
* ```typescript
* const decoder = Decode.optionalField('value', 100, Decode.integer);
*
* decode(decoder, { value: 42 }) === 42
* decode(decoder, {}) === 100
* decode(decoder, 42) // Fails
* ```
*/
export function optionalField<T>(name: string, value: T, child: Decoder<T>) : Decoder<T>
/**
* It's basically the same as {@link Decode.field}, but makes it easier
* to define deep property paths.
* Instead of `Decode.field('outer', Decode.field('inner', Decode.string))`
* you can use `Decode.at(['outer', 'inner'], Decode.string)`
*
* @param path - The property path to follow. The first name is the
* outer-most field
* @param child - The decoder to run on that field
*
* @example
* When you want to access the `name` field in an object like this
* ```json
* {
* "data": {
* "outer": {
* "inner": {
* "name": "Kobe Bryant",
* },
* },
* },
* }
* ```
*
* you would have to chain quite a few {@link Decode.field | field}
* decoders, which is annoying.
*
* ```typescript
* const decoder = Decode.field(
* 'data',
* Decode.field(
* 'outer',
* Decode.field(
* 'inner',
* Decode.field(
* 'name',
* Decode.string,
* ),
* ),
* ),
* );
*
* decode(decoder, raw) === 'Kobe Bryant'
* ```
*
* {@link Decode.at} allows us to be more concise.
*
* ```typescript
* const short = Decode.at(['data', 'outer', 'inner', 'name'], Decode.string);
* decode(short, raw) === 'Kobe Bryant'
* ```
*/
export function at<T>(path: string[], child: Decoder<T>) : Decoder<T>
/**
* This is the same as {@link Decode.at}, but implemented in terms of
* {@link Decode.optionalField} instead of {@link Decode.field}.
* This means that the provided value is returned if any object in the
* given path is missing the next property.
*
* @param path - The property path to follow. The first name is the
* outer-most field
* @param value - The value to use if any field is absent
* @param child - The decoder to run on that field
*
* @example
* ```typescript
* const decoder = Decode.optionalAt(['outer', 'inner', 'value'], 100, Decode.integer);
*
* decode(decoder, { outer: { inner: { value: 42 } } }) === 42
* decode(decoder, { outer: { inner: { } } }) === 100
* decode(decoder, { outer: { } }) === 100
* decode(decoder, {}) === 100
* decode(decoder, 42) // Fails
* decode(decoder, { outer: 42 }) // Fails
* decode(decoder, { outer: { inner: 42 } }) // Fails
* ```
*/
export function optionalAt<T>(name: string, value: T, child: Decoder<T>) : Decoder<T>
/**
* Make a decoder that can be used for decoding arrays, where
* every value is run through the given child decoder.
*
* @param child - Decoder for array items
*
* @example
* Suppose we have a decoder for Users
* ```typescript
* class User {}
*
* const user_decoder = Decode.instance(User, {
* id: Decode.field('id', Decode.integer),
* name: Decode.field('name', Decode.string),
* });
* ```
*
* Using {@link Decode.many} we can easily build a decoder for a list of users:
*
* ```typescript
* const decoder = Decode.many(user_decoder);
*
* decode(decoder, [ {id: 1, name: 'Jeff'}, {id: 2, name: 'Jake'} ]);
* ```
*
* @returns Decoder for an array of things
*/
export function many<T>(child: Decoder<T>) : Decoder<T[]>
/**
* Take the value decoded by the given decoder and transform it.
*
* @param fn - Your mapping function
* @param child - The decoder to run before calling your function
*
* @typeParam A - Input to the mapping function
* @typeParam B - The mapping function returns a value of this type
*
* @example
* ```typescript
* // Add 42 to a number
* const add_42_decoder = Decode.map(value => value + 42, Decode.number);
*
* decode(add_42_decoder, 0) === 42
* decode(add_42_decoder, -42) === 0
* decode(add_42_decoder, 42) === 84
* decode(add_42_decoder, 'text') // Fails
*
* // Convert any value to its string representation.
* const to_string_decoder = Decode.map(value => String(value), Decode.unknown);
*
* decode(to_string_decoder, 42) === '42'
* decode(to_string_decoder, {}) === '[object Object]'
* decode(to_string_decoder, 'text') === 'text'
* decode(to_string_decoder, Symbol('my_symbol')) === 'Symbol(my_symbol)'
* ```
*/
export function map<A, B>(fn: (a: A) => B, child: Decoder<A>) : Decoder<B>
/**
* Similar to {@link Decode.map}, but you return a decoder instead of a
* value.
* This allows you to decide how to continue decoding depending on the
* result.
*
* @param fn - Mapping function that returns a decoder
* @param child - The decoder to run before the mapping function
*
* @typeParam A - Input to the mapping function
* @typeParam B - The mapping function returns a decoder for this type
*
* @example
* Maybe our HTTP API of choice wraps the data in an object that contains
* information about the success of the operation.
* Depending on that value we want to handle the data differently.
*
* ```typescript
* const UserDecoder = Decode.object({
* id: Decode.field('id', Decode.integer),
* name: Decode.field('name', Decode.string),
* });
*
* const response_decoder = Decode.andThen(
* success => {
* if (success) {
* return Decode.field('data', UserDecoder);
* } else {
* return Decode.andThen(
* error => Decode.fail('Got an error response: ' + error),
* Decode.field('error', Decode.string),
* );
* }
* },
* Decode.field('success', Decode.bool),
* );
*
* // Works
* decode(response_decoder, {
* success: true,
* data: {
* id: 1,
* name: 'Kobe Bryant',
* },
* });
*
* // Fails
* decode(response_decoder, {
* success: false,
* error: 'Could not find user!',
* });
* ```
*
* This is nice and all, but it only works for the UserDecoder.
* However, making it generic is rather simple. You just wrap the Decoder
* in a function and accept the data decoder as an argument:
*
* ```typescript
* function responseDecoder<T>(child: Decoder<T>): Decoder<T> {
* return Decode.andThen(success => {
* if (success) {
* return Decode.field('data', child);
* } else {
* // etc.
* }
* }, Decode.field('success', Decode.boolean');
* }
* ```
*/
export function andThen<A, B>(fn: (a: A) => Decoder<B>, child: Decoder<A>) : Decoder<B>
/**
* Combine multiple decoders where any of them can match for the resulting
* decoder to succeed.
*
* @param decoders - A list of decoders that will be executed
*
* @example
* For this example we assume that we have an API endpoint that returns
* either a numeric string or a number. Something like this:
*
* ```json
* [
* { id: 1 },
* { id: '2' },
* ]
* ```
*
* which can be decoded to a flat numeric array like this:
*
* ```typescript
* const string_to_number_decoder = Decode.andThen(str => {
* const v = parseInt(str, 10);
*
* if (typeof v === 'number' && !isNaN(v)) {
* return Decode.succeed(v);
* } else {
* return Decode.fail(expected('a number string', str));
* }
* }, Decode.string);
*
* const decoder = Decode.oneOf([
* Decode.number,
* string_to_number_decoder,
* ]);
*
* // This gives us an array like [1, 2], where both values are actual
* // numbers
* decode(Decode.many(Decode.field('id', decoder)), [
* { id: 1 },
* { id: '2' },
* ]);
* ```
*/
export function oneOf<T>(decoders: Decoder<T>[]) : Decoder<T>
/**
* If the decoder fails, the given value is returned instead.
*
* @example
* ```typescript
* const decoder = Decode.optional(42, Decode.number);
*
* decode(decoder, 100) === 100
* decode(decoder, -10) === -10
*
* decode(decoder, '3') === 42
* decode(decoder, NaN) === 42
* decode(decoder, null) === 42
* ```
*
* @remarks This is implemented by using {@link Decode.oneOf}:
* ```typescript
* function optional(value, child) {
* return Decode.oneOf([ child, Decode.succeed(value) ]);
* }
* ```
*/
export function optional<T>(value: T, child: Decoder<T>) : Decoder<T>
/**
* Make decoder that runs the given function and uses its result for
* decoding.
* This can be used for nested decoding, where you would otherwise get a
* `Cannot access 'value' before initialization` error.
*
* @param fn - Function that returns the actual decoder
*
* @example
* ```typescript
* class Tree {}
*
* const tree_decoder = Decode.instance(Tree, {
* value: Decode.field('value', Decode.integer),
* children: Decode.field(
* 'children',
* Decode.lazy(() => Decode.many(tree_decoder)),
* ),
* });
*
* const raw = {
* value: 42,
* children: [
* { value: 43, children: [] },
* {
* value: 44,
* children: [
* { value: 45, children: [] },
* ],
* },
* ]
* };
*
* decode(tree_decoder, raw) // Decodes the nested tree structure
* ```
*
* @returns A decoder that calls the given function when its executed. The
* returned value is what will then be used for decoding.
*/
export function lazy<T>(fn: () => Decoder<T>) : Decoder<T>
/**
* Make a decoder that *always* succeeds with the given value.
* The input is basically ignored.
*
* @param value - The returned value
*
* @example
* ```typescript
* const decoder = Decode.succeed(42);
*
* decode(decoder, 'string') === 42
* decode(decoder, {}) === 42
* decode(decoder, null) === 42
* decode(decoder, 42) === 42
* ```
*
* @returns A decoder that always succeeds with the given value when executed
*/
export function succeed<T>(value: T) : Decoder<T>
/**
* Make a decoder that *never* succeeds and fails with the given message.
* This is mostly useful for building custom decoders with `andThen`.
*
* @param message - Custom error message
*
* @example
* This example uses {@link expected} to create the error message.
*
* ```typescript
* const base64_decoder = Decode.andThen(str => {
* try {
* return Decode.succeed(atob(str));
* } catch {
* return Decode.fail(expected('a valid base64 string', str));
* }
* }, Decode.string);
*
* decode(base64_decoder, 'SGVsbG8sIFdvcmxkIQ==') === 'Hello, World!'
* decode(base64_decoder, 'invalid$') // Throws
* ```
*
* @returns A decoder that always fails when executed
*/
export function fail<T>(message: string) : Decoder<T>
}
/**
* Run the given decoder on the given input.
*
* @param decoder - The decoder to use
* @param json - The unknown value you want to decode
*
* @typeParam T - The result type of the decoder and also return type
* of the function
*
* @throws If any decoder causes an error this function will throw it
* @returns The value as advertised by the decoder
*/
export function decode<T>(decoder: Decoder<T>, json: any) : T
/**
* This is the same as {@link decode}, but it accepts a JSON string instead of
* a JavaScript value.
*
* @param decoder - The decoder to use
* @param json - The JSON string
*
* @see decode
*/
export function decodeString<T>(decoder: Decoder<T>, json: string) : T
/**
* Useful for building error messages for your own little decoders.
* Since this function is used internally, the message layout will be the same,
* which makes it easier for humans to parse error messages. Especially when
* decoding complex values.
*
* @param description - Describe what kind of value you expected
* @param value - Whatever value you got instead
*
* @returns A nicely formatted error string
*/
export function expected(description: string, value: any) : string