Skip to content

Commit 80850d6

Browse files
author
Elias Mulhall
committed
implement OptionalDecoder class to replace optional
1 parent 3e46cec commit 80850d6

File tree

2 files changed

+73
-42
lines changed

2 files changed

+73
-42
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: 71 additions & 40 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,7 @@ 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';
115135
/**
116136
* The Decoder class constructor is kept private to separate the internal
117137
* `decode` function from the external `run` function. The distinction
@@ -280,15 +300,17 @@ export class Decoder<A> {
280300
let obj: any = {};
281301
for (const key in decoders) {
282302
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) {
303+
// hack: type as any to access the private `decode` method on OptionalDecoder
304+
const decoder: any = decoders[key];
305+
const r = decoder.decode(json[key]);
306+
if (
307+
(r.ok === true && decoder._kind === 'Decoder') ||
308+
(r.ok === true && decoder._kind === 'OptionalDecoder' && r.result !== undefined)
309+
) {
310+
obj[key] = r.result;
311+
} else if (r.ok === false && json[key] === undefined) {
290312
return Result.err({message: `the key '${key}' is required but was not present`});
291-
} else {
313+
} else if (r.ok === false) {
292314
return Result.err(prependAt(`.${key}`, r.error));
293315
}
294316
}
@@ -363,28 +385,6 @@ export class Decoder<A> {
363385
}
364386
});
365387

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-
388388
/**
389389
* Decoder that attempts to run each decoder in `decoders` and either succeeds
390390
* with the first successful decoder, or fails after all decoders have failed.
@@ -655,3 +655,34 @@ export class Decoder<A> {
655655
Result.andThen(value => f(value).decode(json), this.decode(json))
656656
);
657657
}
658+
659+
export class OptionalDecoder<A> {
660+
readonly _kind = 'OptionalDecoder';
661+
662+
private constructor(
663+
private decode: (json: any) => Result.Result<A | undefined, Partial<DecoderError>>
664+
) {}
665+
666+
static optional = <A>(decoder: Decoder<A>): OptionalDecoder<A> =>
667+
new OptionalDecoder(
668+
// hack: type decoder as any to access the private `decode` method on Decoder
669+
(json: any) => (json === undefined ? Result.ok(undefined) : (decoder as any).decode(json))
670+
);
671+
672+
map = <B>(f: (value: A) => B): OptionalDecoder<B> =>
673+
new OptionalDecoder<B>((json: any) =>
674+
Result.map(
675+
(value: A | undefined) => (value === undefined ? undefined : f(value)),
676+
this.decode(json)
677+
)
678+
);
679+
680+
andThen = <B>(f: (value: A) => Decoder<B>): OptionalDecoder<B> =>
681+
new OptionalDecoder<B>((json: any) =>
682+
Result.andThen(
683+
(value: A | undefined) =>
684+
value === undefined ? Result.ok(undefined) : (f(value) as any).decode(json),
685+
this.decode(json)
686+
)
687+
);
688+
}

0 commit comments

Comments
 (0)