Skip to content

Commit c34f12a

Browse files
author
Elias Mulhall
committed
implement OptionalDecoder class to replace optional
1 parent 5c53c44 commit c34f12a

File tree

3 files changed

+144
-47
lines changed

3 files changed

+144
-47
lines changed

src/combinators.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Decoder} from './decoder';
1+
import {Decoder, OptionalDecoder} from './decoder';
22

33
/** See `Decoder.string` */
44
export function string(): Decoder<string> {
@@ -36,7 +36,7 @@ export const array: <A>(decoder: Decoder<A>) => Decoder<A[]> = Decoder.array;
3636
export const dict: <A>(decoder: Decoder<A>) => Decoder<{[name: string]: A}> = Decoder.dict;
3737

3838
/** See `Decoder.optional` */
39-
export const optional = Decoder.optional;
39+
export const optional = OptionalDecoder.optional;
4040

4141
/** See `Decoder.oneOf` */
4242
export const oneOf: <A>(...decoders: Decoder<A>[]) => Decoder<A> = Decoder.oneOf;

src/decoder.ts

Lines changed: 130 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,44 @@ export interface DecoderError {
1414
}
1515

1616
/**
17-
* Defines a mapped type over an interface `A`. `DecoderObject<A>` is an
18-
* interface that has all the keys or `A`, but each key's property type is
19-
* mapped to a decoder for that type. This type is used when creating decoders
20-
* for objects.
17+
* Helper type with no semantic meaning, used as part of a trick in
18+
* `DecoderObject` to distinguish between optional properties and properties
19+
* that may have a value of undefined, but aren't optional.
20+
*/
21+
type HideUndefined<T> = {};
22+
23+
/**
24+
* Defines a mapped type over an interface `A`. This type is used when creating
25+
* decoders for objects.
26+
*
27+
* `DecoderObject<A>` is an interface that has all the properties or `A`, but
28+
* each property's type is mapped to a decoder for that type. If a property is
29+
* required in `A`, the decoder type is `Decoder<proptype>`. If a property is
30+
* optional in `A`, then that property is required in `DecoderObject<A>`, but
31+
* the decoder type is `OptionalDecoder<proptype> | Decoder<proptype>`.
32+
*
33+
* The `OptionalDecoder` type is only returned by the `optional` decoder.
2134
*
2235
* Example:
2336
* ```
24-
* interface X {
37+
* interface ABC {
2538
* a: boolean;
26-
* b: string;
39+
* b?: string;
40+
* c: number | undefined;
2741
* }
2842
*
29-
* const decoderObject: DecoderObject<X> = {
30-
* a: boolean(),
31-
* b: string()
43+
* DecoderObject<ABC> === {
44+
* a: Decoder<boolean>;
45+
* b: OptionalDecoder<string> | Decoder<string>;
46+
* c: Decoder<number | undefined>;
3247
* }
3348
* ```
3449
*/
35-
export type DecoderObject<A> = {[t in keyof A]: Decoder<A[t]>};
50+
export type DecoderObject<T> = {
51+
[P in keyof T]-?: undefined extends {[Q in keyof T]: HideUndefined<T[Q]>}[P]
52+
? OptionalDecoder<Exclude<T[P], undefined>> | Decoder<Exclude<T[P], undefined>>
53+
: Decoder<T[P]>
54+
};
3655

3756
/**
3857
* Type guard for `DecoderError`. One use case of the type guard is in the
@@ -112,6 +131,8 @@ const prependAt = (newAt: string, {at, ...rest}: Partial<DecoderError>): Partial
112131
* things with a `Result` as with the decoder methods.
113132
*/
114133
export class Decoder<A> {
134+
readonly _kind = 'Decoder';
135+
115136
/**
116137
* The Decoder class constructor is kept private to separate the internal
117138
* `decode` function from the external `run` function. The distinction
@@ -215,12 +236,13 @@ export class Decoder<A> {
215236
* isBig: boolean;
216237
* }
217238
*
218-
* const bearDecoder1: Decoder<Bear> = object({
239+
* const bearDecoder1 = object<Bear>({
219240
* kind: constant('bear'),
220241
* isBig: boolean()
221242
* });
222-
* // Type 'Decoder<{ kind: string; isBig: boolean; }>' is not assignable to
223-
* // type 'Decoder<Bear>'. Type 'string' is not assignable to type '"bear"'.
243+
* // Types of property 'kind' are incompatible.
244+
* // Type 'Decoder<string>' is not assignable to type 'Decoder<"bear">'.
245+
* // Type 'string' is not assignable to type '"bear"'.
224246
*
225247
* const bearDecoder2: Decoder<Bear> = object({
226248
* kind: constant<'bear'>('bear'),
@@ -280,15 +302,17 @@ export class Decoder<A> {
280302
let obj: any = {};
281303
for (const key in decoders) {
282304
if (decoders.hasOwnProperty(key)) {
283-
const r = decoders[key].decode(json[key]);
284-
if (r.ok === true) {
285-
// tslint:disable-next-line:strict-type-predicates
286-
if (r.result !== undefined) {
287-
obj[key] = r.result;
288-
}
289-
} else if (json[key] === undefined) {
305+
// hack: type as any to access the private `decode` method on OptionalDecoder
306+
const decoder: any = decoders[key];
307+
const r = decoder.decode(json[key]);
308+
if (
309+
(r.ok === true && decoder._kind === 'Decoder') ||
310+
(r.ok === true && decoder._kind === 'OptionalDecoder' && r.result !== undefined)
311+
) {
312+
obj[key] = r.result;
313+
} else if (r.ok === false && json[key] === undefined) {
290314
return Result.err({message: `the key '${key}' is required but was not present`});
291-
} else {
315+
} else if (r.ok === false) {
292316
return Result.err(prependAt(`.${key}`, r.error));
293317
}
294318
}
@@ -363,28 +387,6 @@ export class Decoder<A> {
363387
}
364388
});
365389

366-
/**
367-
* Decoder for values that may be `undefined`. This is primarily helpful for
368-
* decoding interfaces with optional fields.
369-
*
370-
* Example:
371-
* ```
372-
* interface User {
373-
* id: number;
374-
* isOwner?: boolean;
375-
* }
376-
*
377-
* const decoder: Decoder<User> = object({
378-
* id: number(),
379-
* isOwner: optional(boolean())
380-
* });
381-
* ```
382-
*/
383-
static optional = <A>(decoder: Decoder<A>): Decoder<undefined | A> =>
384-
new Decoder<undefined | A>(
385-
(json: any) => (json === undefined ? Result.ok(undefined) : decoder.decode(json))
386-
);
387-
388390
/**
389391
* Decoder that attempts to run each decoder in `decoders` and either succeeds
390392
* with the first successful decoder, or fails after all decoders have failed.
@@ -655,3 +657,88 @@ export class Decoder<A> {
655657
Result.andThen(value => f(value).decode(json), this.decode(json))
656658
);
657659
}
660+
661+
/**
662+
* The `optional` decoder is given it's own type, the `OptionalDecoder` type,
663+
* since it behaves differently from the other decoders. This decoder has no
664+
* `run` method, so it can't be directly used to test a value. Instead, the
665+
* `object` decoder accepts `optional` for decoding object properties that have
666+
* been marked as optional with the `field?: value` notation.
667+
*/
668+
export class OptionalDecoder<A> {
669+
readonly _kind = 'OptionalDecoder';
670+
671+
private constructor(
672+
private decode: (json: any) => Result.Result<A | undefined, Partial<DecoderError>>
673+
) {}
674+
675+
/**
676+
* Decoder to designate that a property may not be present in an object. The
677+
* behavior of `optional` is distinct from using `constant(undefined)` in
678+
* that when the property is not found in the input, the key will not be
679+
* present in the decoded value.
680+
*
681+
* Example:
682+
* ```
683+
* // type with explicit undefined property
684+
* interface Breakfast1 {
685+
* eggs: number;
686+
* withBacon: boolean | undefined;
687+
* }
688+
*
689+
* // type with optional property
690+
* interface Breakfast2 {
691+
* eggs: number;
692+
* withBacon?: boolean;
693+
* }
694+
*
695+
* // in the first case we can't use `optional`
696+
* breakfast1Decoder = object<Breakfast1>({
697+
* eggs: number(),
698+
* withBacon: union(boolean(), constant(undefined))
699+
* });
700+
*
701+
* // in the second case we can
702+
* breakfast2Decoder = object<Breakfast2>({
703+
* eggs: number(),
704+
* withBacon: optional(boolean())
705+
* });
706+
*
707+
* breakfast1Decoder.run({eggs: 12})
708+
* // => {ok: true, result: {eggs: 12, withBacon: undefined}}
709+
*
710+
* breakfast2Decoder.run({eggs: 7})
711+
* // => {ok: true, result: {eggs: 7}}
712+
* ```
713+
*/
714+
static optional = <A>(decoder: Decoder<A>): OptionalDecoder<A> =>
715+
new OptionalDecoder(
716+
// hack: type decoder as any to access the private `decode` method on Decoder
717+
(json: any) => (json === undefined ? Result.ok(undefined) : (decoder as any).decode(json))
718+
);
719+
720+
/**
721+
* See `Decoder.prototype.map`. The function `f` is only executed if the
722+
* optional decoder successfuly finds and decodes a value.
723+
*/
724+
map = <B>(f: (value: A) => B): OptionalDecoder<B> =>
725+
new OptionalDecoder<B>((json: any) =>
726+
Result.map(
727+
(value: A | undefined) => (value === undefined ? undefined : f(value)),
728+
this.decode(json)
729+
)
730+
);
731+
732+
/**
733+
* See `Decoder.prototype.andThen`. The function `f` is only executed if the
734+
* optional decoder successfuly finds and decodes a value.
735+
*/
736+
andThen = <B>(f: (value: A) => Decoder<B>): OptionalDecoder<B> =>
737+
new OptionalDecoder<B>((json: any) =>
738+
Result.andThen(
739+
(value: A | undefined) =>
740+
value === undefined ? Result.ok(undefined) : (f(value) as any).decode(json),
741+
this.decode(json)
742+
)
743+
);
744+
}

test/json-decode.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,18 @@ describe('object', () => {
179179
});
180180

181181
it('can decode a nested object', () => {
182-
const decoder = object({
183-
payload: object({x: number(), y: number()}),
182+
interface Point {
183+
x: number;
184+
y: number;
185+
}
186+
187+
interface Location {
188+
payload: Point;
189+
error: false;
190+
}
191+
192+
const decoder = object<Location>({
193+
payload: object<Point>({x: number(), y: number()}),
184194
error: constant(false)
185195
});
186196
const json = {payload: {x: 5, y: 2}, error: false};

0 commit comments

Comments
 (0)