Skip to content

Commit b02d03c

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

File tree

2 files changed

+71
-42
lines changed

2 files changed

+71
-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: 69 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,16 @@ 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+
const decoder: any = decoders[key];
304+
const r = decoder.decode(json[key]);
305+
if (
306+
(r.ok === true && decoder._kind === 'Decoder') ||
307+
(r.ok === true && decoder._kind === 'OptionalDecoder' && r.result !== undefined)
308+
) {
309+
obj[key] = r.result;
310+
} else if (r.ok === false && json[key] === undefined) {
290311
return Result.err({message: `the key '${key}' is required but was not present`});
291-
} else {
312+
} else if (r.ok === false) {
292313
return Result.err(prependAt(`.${key}`, r.error));
293314
}
294315
}
@@ -363,28 +384,6 @@ export class Decoder<A> {
363384
}
364385
});
365386

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

0 commit comments

Comments
 (0)