diff --git a/docs/classes/_decoder_.decoder.md b/docs/classes/_decoder_.decoder.md
index d1bf935..f1a715c 100644
--- a/docs/classes/_decoder_.decoder.md
+++ b/docs/classes/_decoder_.decoder.md
@@ -18,10 +18,6 @@ Alternatively, the main decoder `run()` method returns an object of type `Result
## Index
-### Constructors
-
-* [constructor](_decoder_.decoder.md#constructor)
-
### Properties
* [decode](_decoder_.decoder.md#decode)
@@ -43,7 +39,6 @@ Alternatively, the main decoder `run()` method returns an object of type `Result
* [number](_decoder_.decoder.md#number)
* [object](_decoder_.decoder.md#object)
* [oneOf](_decoder_.decoder.md#oneof)
-* [optional](_decoder_.decoder.md#optional)
* [string](_decoder_.decoder.md#string)
* [succeed](_decoder_.decoder.md#succeed)
* [union](_decoder_.decoder.md#union)
@@ -52,28 +47,6 @@ Alternatively, the main decoder `run()` method returns an object of type `Result
---
-## Constructors
-
-
-
-### `` constructor
-
-⊕ **new Decoder**(decode: *`function`*): [Decoder](_decoder_.decoder.md)
-
-The Decoder class constructor is kept private to separate the internal `decode` function from the external `run` function. The distinction between the two functions is that `decode` returns a `Partial` on failure, which contains an unfinished error report. When `run` is called on a decoder, the relevant series of `decode` calls is made, and then on failure the resulting `Partial` is turned into a `DecoderError` by filling in the missing informaiton.
-
-While hiding the constructor may seem restrictive, leveraging the provided decoder combinators and helper functions such as `andThen` and `map` should be enough to build specialized decoders as needed.
-
-**Parameters:**
-
-| Param | Type |
-| ------ | ------ |
-| decode | `function` |
-
-**Returns:** [Decoder](_decoder_.decoder.md)
-
-___
-
## Properties
@@ -331,6 +304,7 @@ Providing the type parameter is only necessary for type-literal strings and numb
| constant(true) | Decoder |
| constant(false) | Decoder |
| constant(null) | Decoder |
+ | constant(undefined) | Decoder |
| constant('alaska') | Decoder |
| constant<'alaska'>('alaska') | Decoder<'alaska'> |
| constant(50) | Decoder |
@@ -349,12 +323,13 @@ interface Bear {
isBig: boolean;
}
-const bearDecoder1: Decoder = object({
+const bearDecoder1 = object({
kind: constant('bear'),
isBig: boolean()
});
-// Type 'Decoder<{ kind: string; isBig: boolean; }>' is not assignable to
-// type 'Decoder'. Type 'string' is not assignable to type '"bear"'.
+// Types of property 'kind' are incompatible.
+// Type 'Decoder' is not assignable to type 'Decoder<"bear">'.
+// Type 'string' is not assignable to type '"bear"'.
const bearDecoder2: Decoder = object({
kind: constant<'bear'>('bear'),
@@ -561,40 +536,6 @@ oneOf(constant('start'), constant('stop'), succeed('unknown'))
**Returns:** [Decoder](_decoder_.decoder.md)<`A`>
-___
-
-
-### `` optional
-
-▸ **optional**A(decoder: *[Decoder](_decoder_.decoder.md)<`A`>*): [Decoder](_decoder_.decoder.md)< `undefined` | `A`>
-
-Decoder for values that may be `undefined`. This is primarily helpful for decoding interfaces with optional fields.
-
-Example:
-
-```
-interface User {
- id: number;
- isOwner?: boolean;
-}
-
-const decoder: Decoder = object({
- id: number(),
- isOwner: optional(boolean())
-});
-```
-
-**Type parameters:**
-
-#### A
-**Parameters:**
-
-| Param | Type |
-| ------ | ------ |
-| decoder | [Decoder](_decoder_.decoder.md)<`A`> |
-
-**Returns:** [Decoder](_decoder_.decoder.md)< `undefined` | `A`>
-
___
@@ -812,18 +753,18 @@ decoder.run({a: {x: 'cats'}})
// => {ok: false, error: {... at: 'input.a.b[0]' message: 'path does not exist'}}
```
-Note that the `decoder` is ran on the value found at the last key in the path, even if the last key is not found. This allows the `optional` decoder to succeed when appropriate.
+Note that the `decoder` is ran on the value found at the last key in the path, even if the last key is not found. This allows the value to be `undefined` when appropriate.
```
-const optionalDecoder = valueAt(['a', 'b', 'c'], optional(string()));
+const decoder = valueAt(['a', 'b', 'c'], union(string(), constant(undefined)));
-optionalDecoder.run({a: {b: {c: 'surprise!'}}})
+decoder.run({a: {b: {c: 'surprise!'}}})
// => {ok: true, result: 'surprise!'}
-optionalDecoder.run({a: {b: 'cats'}})
+decoder.run({a: {b: 'cats'}})
// => {ok: false, error: {... at: 'input.a.b.c' message: 'expected an object, got "cats"'}
-optionalDecoder.run({a: {b: {z: 1}}})
+decoder.run({a: {b: {z: 1}}})
// => {ok: true, result: undefined}
```
diff --git a/docs/classes/_decoder_.optionaldecoder.md b/docs/classes/_decoder_.optionaldecoder.md
new file mode 100644
index 0000000..b235b8a
--- /dev/null
+++ b/docs/classes/_decoder_.optionaldecoder.md
@@ -0,0 +1,144 @@
+[@mojotech/json-type-validation](../README.md) > ["decoder"](../modules/_decoder_.md) > [OptionalDecoder](../classes/_decoder_.optionaldecoder.md)
+
+# Class: OptionalDecoder
+
+The `optional` decoder is given it's own type, the `OptionalDecoder` type, since it behaves differently from the other decoders. This decoder has no `run` method, so it can't be directly used to test a value. Instead, the `object` decoder accepts `optional` for decoding object properties that have been marked as optional with the `field?: value` notation.
+
+## Type parameters
+#### A
+## Hierarchy
+
+**OptionalDecoder**
+
+## Index
+
+### Properties
+
+* [decode](_decoder_.optionaldecoder.md#decode)
+
+### Methods
+
+* [andThen](_decoder_.optionaldecoder.md#andthen)
+* [map](_decoder_.optionaldecoder.md#map)
+* [optional](_decoder_.optionaldecoder.md#optional)
+
+---
+
+## Properties
+
+
+
+### `` decode
+
+**● decode**: *`function`*
+
+#### Type declaration
+▸(json: *`any`*): `Result.Result`< `A` | `undefined`, `Partial`<[DecoderError](../interfaces/_decoder_.decodererror.md)>>
+
+**Parameters:**
+
+| Param | Type |
+| ------ | ------ |
+| json | `any` |
+
+**Returns:** `Result.Result`< `A` | `undefined`, `Partial`<[DecoderError](../interfaces/_decoder_.decodererror.md)>>
+
+___
+
+## Methods
+
+
+
+### andThen
+
+▸ **andThen**B(f: *`function`*): [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`>
+
+See `Decoder.prototype.andThen`. The function `f` is only executed if the optional decoder successfuly finds and decodes a value.
+
+**Type parameters:**
+
+#### B
+**Parameters:**
+
+| Param | Type |
+| ------ | ------ |
+| f | `function` |
+
+**Returns:** [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`>
+
+___
+
+
+### map
+
+▸ **map**B(f: *`function`*): [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`>
+
+See `Decoder.prototype.map`. The function `f` is only executed if the optional decoder successfuly finds and decodes a value.
+
+**Type parameters:**
+
+#### B
+**Parameters:**
+
+| Param | Type |
+| ------ | ------ |
+| f | `function` |
+
+**Returns:** [OptionalDecoder](_decoder_.optionaldecoder.md)<`B`>
+
+___
+
+
+### `` optional
+
+▸ **optional**A(decoder: *[Decoder](_decoder_.decoder.md)<`A`>*): [OptionalDecoder](_decoder_.optionaldecoder.md)<`A`>
+
+Decoder to designate that a property may not be present in an object. The behavior of `optional` is distinct from using `constant(undefined)` in that when the property is not found in the input, the key will not be present in the decoded value.
+
+Example:
+
+```
+// type with explicit undefined property
+interface Breakfast1 {
+ eggs: number;
+ withBacon: boolean | undefined;
+}
+
+// type with optional property
+interface Breakfast2 {
+ eggs: number;
+ withBacon?: boolean;
+}
+
+// in the first case we can't use `optional`
+breakfast1Decoder = object({
+ eggs: number(),
+ withBacon: union(boolean(), constant(undefined))
+});
+
+// in the second case we can
+breakfast2Decoder = object({
+ eggs: number(),
+ withBacon: optional(boolean())
+});
+
+breakfast1Decoder.run({eggs: 12})
+// => {ok: true, result: {eggs: 12, withBacon: undefined}}
+
+breakfast2Decoder.run({eggs: 7})
+// => {ok: true, result: {eggs: 7}}
+```
+
+**Type parameters:**
+
+#### A
+**Parameters:**
+
+| Param | Type |
+| ------ | ------ |
+| decoder | [Decoder](_decoder_.decoder.md)<`A`> |
+
+**Returns:** [OptionalDecoder](_decoder_.optionaldecoder.md)<`A`>
+
+___
+
diff --git a/docs/modules/_combinators_.md b/docs/modules/_combinators_.md
index b472855..561727d 100644
--- a/docs/modules/_combinators_.md
+++ b/docs/modules/_combinators_.md
@@ -11,6 +11,7 @@
* [dict](_combinators_.md#dict)
* [fail](_combinators_.md#fail)
* [lazy](_combinators_.md#lazy)
+* [object](_combinators_.md#object)
* [oneOf](_combinators_.md#oneof)
* [optional](_combinators_.md#optional)
* [succeed](_combinators_.md#succeed)
@@ -22,7 +23,6 @@
* [boolean](_combinators_.md#boolean)
* [constant](_combinators_.md#constant)
* [number](_combinators_.md#number)
-* [object](_combinators_.md#object)
* [string](_combinators_.md#string)
* [union](_combinators_.md#union)
@@ -135,6 +135,15 @@ See `Decoder.lazy`
**Returns:** [Decoder](../classes/_decoder_.decoder.md)<`A`>
+___
+
+
+### `` object
+
+**● object**: *[object](../classes/_decoder_.decoder.md#object)* = Decoder.object
+
+See `Decoder.object`
+
___
@@ -163,24 +172,10 @@ ___
### `` optional
-**● optional**: *`function`* = Decoder.optional
+**● optional**: *[optional]()* = OptionalDecoder.optional
See `Decoder.optional`
-#### Type declaration
-▸A(decoder: *[Decoder](../classes/_decoder_.decoder.md)<`A`>*): [Decoder](../classes/_decoder_.decoder.md)< `A` | `undefined`>
-
-**Type parameters:**
-
-#### A
-**Parameters:**
-
-| Param | Type |
-| ------ | ------ |
-| decoder | [Decoder](../classes/_decoder_.decoder.md)<`A`> |
-
-**Returns:** [Decoder](../classes/_decoder_.decoder.md)< `A` | `undefined`>
-
___
@@ -319,26 +314,6 @@ See `Decoder.number`
**Returns:** [Decoder](../classes/_decoder_.decoder.md)<`number`>
-___
-
-
-### object
-
-▸ **object**A(decoders: *[DecoderObject](_decoder_.md#decoderobject)<`A`>*): [Decoder](../classes/_decoder_.decoder.md)<`A`>
-
-See `Decoder.object`
-
-**Type parameters:**
-
-#### A
-**Parameters:**
-
-| Param | Type |
-| ------ | ------ |
-| decoders | [DecoderObject](_decoder_.md#decoderobject)<`A`> |
-
-**Returns:** [Decoder](../classes/_decoder_.decoder.md)<`A`>
-
___
diff --git a/docs/modules/_decoder_.md b/docs/modules/_decoder_.md
index dc101d2..47d220e 100644
--- a/docs/modules/_decoder_.md
+++ b/docs/modules/_decoder_.md
@@ -7,6 +7,7 @@
### Classes
* [Decoder](../classes/_decoder_.decoder.md)
+* [OptionalDecoder](../classes/_decoder_.optionaldecoder.md)
### Interfaces
@@ -31,19 +32,25 @@
**ΤDecoderObject**: *`object`*
-Defines a mapped type over an interface `A`. `DecoderObject` is an interface that has all the keys or `A`, but each key's property type is mapped to a decoder for that type. This type is used when creating decoders for objects.
+Defines a mapped type over an interface `A`. This type is used when creating decoders for objects.
+
+`DecoderObject` is an interface that has all the properties or `A`, but each property's type is mapped to a decoder for that type. If a property is required in `A`, the decoder type is `Decoder`. If a property is optional in `A`, then that property is required in `DecoderObject`, but the decoder type is `OptionalDecoder | Decoder`.
+
+The `OptionalDecoder` type is only returned by the `optional` decoder.
Example:
```
-interface X {
+interface ABC {
a: boolean;
- b: string;
+ b?: string;
+ c: number | undefined;
}
-const decoderObject: DecoderObject = {
- a: boolean(),
- b: string()
+DecoderObject === {
+ a: Decoder;
+ b: OptionalDecoder | Decoder;
+ c: Decoder;
}
```
diff --git a/package.json b/package.json
index 7c4a72b..9f15e85 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"tslint": "^5.8.0",
"tslint-config-prettier": "^1.1.0",
"tslint-config-standard": "^7.0.0",
- "typedoc": "^0.11.1",
+ "typedoc": "^0.12.0",
"typedoc-plugin-markdown": "^1.0.13",
"typescript": "~2.9.2"
}
diff --git a/src/combinators.ts b/src/combinators.ts
index 4442e52..59e92bc 100644
--- a/src/combinators.ts
+++ b/src/combinators.ts
@@ -1,4 +1,4 @@
-import {Decoder, DecoderObject} from './decoder';
+import {Decoder, OptionalDecoder} from './decoder';
/** See `Decoder.string` */
export function string(): Decoder {
@@ -27,9 +27,7 @@ export function constant(value: any): Decoder {
}
/** See `Decoder.object` */
-export function object(decoders: DecoderObject): Decoder {
- return Decoder.object(decoders);
-}
+export const object = Decoder.object;
/** See `Decoder.array` */
export const array: (decoder: Decoder) => Decoder = Decoder.array;
@@ -38,7 +36,7 @@ export const array: (decoder: Decoder) => Decoder = Decoder.array;
export const dict: (decoder: Decoder) => Decoder<{[name: string]: A}> = Decoder.dict;
/** See `Decoder.optional` */
-export const optional: (decoder: Decoder) => Decoder = Decoder.optional;
+export const optional = OptionalDecoder.optional;
/** See `Decoder.oneOf` */
export const oneOf: (...decoders: Decoder[]) => Decoder = Decoder.oneOf;
diff --git a/src/decoder.ts b/src/decoder.ts
index 9b2bfb7..e57817a 100644
--- a/src/decoder.ts
+++ b/src/decoder.ts
@@ -14,25 +14,44 @@ export interface DecoderError {
}
/**
- * Defines a mapped type over an interface `A`. `DecoderObject` is an
- * interface that has all the keys or `A`, but each key's property type is
- * mapped to a decoder for that type. This type is used when creating decoders
- * for objects.
+ * Helper type with no semantic meaning, used as part of a trick in
+ * `DecoderObject` to distinguish between optional properties and properties
+ * that may have a value of undefined, but aren't optional.
+ */
+type HideUndefined = {};
+
+/**
+ * Defines a mapped type over an interface `A`. This type is used when creating
+ * decoders for objects.
+ *
+ * `DecoderObject` is an interface that has all the properties or `A`, but
+ * each property's type is mapped to a decoder for that type. If a property is
+ * required in `A`, the decoder type is `Decoder`. If a property is
+ * optional in `A`, then that property is required in `DecoderObject`, but
+ * the decoder type is `OptionalDecoder | Decoder`.
+ *
+ * The `OptionalDecoder` type is only returned by the `optional` decoder.
*
* Example:
* ```
- * interface X {
+ * interface ABC {
* a: boolean;
- * b: string;
+ * b?: string;
+ * c: number | undefined;
* }
*
- * const decoderObject: DecoderObject = {
- * a: boolean(),
- * b: string()
+ * DecoderObject === {
+ * a: Decoder;
+ * b: OptionalDecoder | Decoder;
+ * c: Decoder;
* }
* ```
*/
-export type DecoderObject = {[t in keyof A]: Decoder};
+export type DecoderObject = {
+ [P in keyof T]-?: undefined extends {[Q in keyof T]: HideUndefined}[P]
+ ? OptionalDecoder> | Decoder>
+ : Decoder
+};
/**
* Type guard for `DecoderError`. One use case of the type guard is in the
@@ -112,6 +131,11 @@ const prependAt = (newAt: string, {at, ...rest}: Partial): Partial
* things with a `Result` as with the decoder methods.
*/
export class Decoder {
+ /**
+ * @hidden
+ */
+ readonly _kind = 'Decoder';
+
/**
* The Decoder class constructor is kept private to separate the internal
* `decode` function from the external `run` function. The distinction
@@ -125,6 +149,8 @@ export class Decoder {
* provided decoder combinators and helper functions such as
* `andThen` and `map` should be enough to build specialized decoders as
* needed.
+ *
+ * @hidden
*/
private constructor(private decode: (json: any) => Result.Result>) {}
@@ -197,6 +223,7 @@ export class Decoder {
* | constant(true) | Decoder |
* | constant(false) | Decoder |
* | constant(null) | Decoder |
+ * | constant(undefined) | Decoder |
* | constant('alaska') | Decoder |
* | constant<'alaska'>('alaska') | Decoder<'alaska'> |
* | constant(50) | Decoder |
@@ -215,12 +242,13 @@ export class Decoder {
* isBig: boolean;
* }
*
- * const bearDecoder1: Decoder = object({
+ * const bearDecoder1 = object({
* kind: constant('bear'),
* isBig: boolean()
* });
- * // Type 'Decoder<{ kind: string; isBig: boolean; }>' is not assignable to
- * // type 'Decoder'. Type 'string' is not assignable to type '"bear"'.
+ * // Types of property 'kind' are incompatible.
+ * // Type 'Decoder' is not assignable to type 'Decoder<"bear">'.
+ * // Type 'string' is not assignable to type '"bear"'.
*
* const bearDecoder2: Decoder = object({
* kind: constant<'bear'>('bear'),
@@ -280,15 +308,17 @@ export class Decoder {
let obj: any = {};
for (const key in decoders) {
if (decoders.hasOwnProperty(key)) {
- const r = decoders[key].decode(json[key]);
- if (r.ok === true) {
- // tslint:disable-next-line:strict-type-predicates
- if (r.result !== undefined) {
- obj[key] = r.result;
- }
- } else if (json[key] === undefined) {
+ // hack: type as any to access the private `decode` method on OptionalDecoder
+ const decoder: any = decoders[key];
+ const r = decoder.decode(json[key]);
+ if (
+ (r.ok === true && decoder._kind === 'Decoder') ||
+ (r.ok === true && decoder._kind === 'OptionalDecoder' && r.result !== undefined)
+ ) {
+ obj[key] = r.result;
+ } else if (r.ok === false && json[key] === undefined) {
return Result.err({message: `the key '${key}' is required but was not present`});
- } else {
+ } else if (r.ok === false) {
return Result.err(prependAt(`.${key}`, r.error));
}
}
@@ -363,28 +393,6 @@ export class Decoder {
}
});
- /**
- * Decoder for values that may be `undefined`. This is primarily helpful for
- * decoding interfaces with optional fields.
- *
- * Example:
- * ```
- * interface User {
- * id: number;
- * isOwner?: boolean;
- * }
- *
- * const decoder: Decoder = object({
- * id: number(),
- * isOwner: optional(boolean())
- * });
- * ```
- */
- static optional = (decoder: Decoder): Decoder =>
- new Decoder(
- (json: any) => (json === undefined ? Result.ok(undefined) : decoder.decode(json))
- );
-
/**
* Decoder that attempts to run each decoder in `decoders` and either succeeds
* with the first successful decoder, or fails after all decoders have failed.
@@ -472,18 +480,18 @@ export class Decoder {
* ```
*
* Note that the `decoder` is ran on the value found at the last key in the
- * path, even if the last key is not found. This allows the `optional`
- * decoder to succeed when appropriate.
+ * path, even if the last key is not found. This allows the value to be
+ * `undefined` when appropriate.
* ```
- * const optionalDecoder = valueAt(['a', 'b', 'c'], optional(string()));
+ * const decoder = valueAt(['a', 'b', 'c'], union(string(), constant(undefined)));
*
- * optionalDecoder.run({a: {b: {c: 'surprise!'}}})
+ * decoder.run({a: {b: {c: 'surprise!'}}})
* // => {ok: true, result: 'surprise!'}
*
- * optionalDecoder.run({a: {b: 'cats'}})
+ * decoder.run({a: {b: 'cats'}})
* // => {ok: false, error: {... at: 'input.a.b.c' message: 'expected an object, got "cats"'}
*
- * optionalDecoder.run({a: {b: {z: 1}}})
+ * decoder.run({a: {b: {z: 1}}})
* // => {ok: true, result: undefined}
* ```
*/
@@ -655,3 +663,94 @@ export class Decoder {
Result.andThen(value => f(value).decode(json), this.decode(json))
);
}
+
+/**
+ * The `optional` decoder is given it's own type, the `OptionalDecoder` type,
+ * since it behaves differently from the other decoders. This decoder has no
+ * `run` method, so it can't be directly used to test a value. Instead, the
+ * `object` decoder accepts `optional` for decoding object properties that have
+ * been marked as optional with the `field?: value` notation.
+ */
+export class OptionalDecoder {
+ /**
+ * @hidden
+ */
+ readonly _kind = 'OptionalDecoder';
+
+ /**
+ * @hidden
+ */
+ private constructor(
+ private decode: (json: any) => Result.Result>
+ ) {}
+
+ /**
+ * Decoder to designate that a property may not be present in an object. The
+ * behavior of `optional` is distinct from using `constant(undefined)` in
+ * that when the property is not found in the input, the key will not be
+ * present in the decoded value.
+ *
+ * Example:
+ * ```
+ * // type with explicit undefined property
+ * interface Breakfast1 {
+ * eggs: number;
+ * withBacon: boolean | undefined;
+ * }
+ *
+ * // type with optional property
+ * interface Breakfast2 {
+ * eggs: number;
+ * withBacon?: boolean;
+ * }
+ *
+ * // in the first case we can't use `optional`
+ * breakfast1Decoder = object({
+ * eggs: number(),
+ * withBacon: union(boolean(), constant(undefined))
+ * });
+ *
+ * // in the second case we can
+ * breakfast2Decoder = object({
+ * eggs: number(),
+ * withBacon: optional(boolean())
+ * });
+ *
+ * breakfast1Decoder.run({eggs: 12})
+ * // => {ok: true, result: {eggs: 12, withBacon: undefined}}
+ *
+ * breakfast2Decoder.run({eggs: 7})
+ * // => {ok: true, result: {eggs: 7}}
+ * ```
+ */
+ static optional = (decoder: Decoder): OptionalDecoder =>
+ new OptionalDecoder(
+ // hack: type decoder as any to access the private `decode` method on Decoder
+ (json: any) => (json === undefined ? Result.ok(undefined) : (decoder as any).decode(json))
+ );
+
+ /**
+ * See `Decoder.prototype.map`. The function `f` is only executed if the
+ * optional decoder successfuly finds and decodes a value.
+ */
+ map = (f: (value: A) => B): OptionalDecoder =>
+ new OptionalDecoder((json: any) =>
+ Result.map(
+ (value: A | undefined) => (value === undefined ? undefined : f(value)),
+ this.decode(json)
+ )
+ );
+
+ /**
+ * See `Decoder.prototype.andThen`. The function `f` is only executed if the
+ * optional decoder successfuly finds and decodes a value.
+ */
+ andThen = (f: (value: A) => Decoder): OptionalDecoder =>
+ new OptionalDecoder((json: any) =>
+ Result.andThen(
+ (value: A | undefined) =>
+ value === undefined ? Result.ok(undefined) : (f(value) as any).decode(json),
+ this.decode(json)
+ )
+ );
+}
diff --git a/test/json-decode.test.ts b/test/json-decode.test.ts
index 8cdfe88..a5f644b 100644
--- a/test/json-decode.test.ts
+++ b/test/json-decode.test.ts
@@ -124,7 +124,7 @@ describe('constant', () => {
interface TrueValue {
x: true;
}
- const decoder: Decoder = object({x: constant(true)});
+ const decoder: Decoder = object({x: constant(true)});
expect(decoder.run({x: true})).toEqual({ok: true, result: {x: true}});
});
@@ -133,7 +133,7 @@ describe('constant', () => {
interface FalseValue {
x: false;
}
- const decoder: Decoder = object({x: constant(false)});
+ const decoder = object({x: constant(false)});
expect(decoder.run({x: false})).toEqual({ok: true, result: {x: false}});
});
@@ -142,11 +142,27 @@ describe('constant', () => {
interface NullValue {
x: null;
}
- const decoder: Decoder = object({x: constant(null)});
+ const decoder = object({x: constant(null)});
expect(decoder.run({x: null})).toEqual({ok: true, result: {x: null}});
});
+ it('can decode undefined', () => {
+ interface UndefinedValue {
+ a: string;
+ b: undefined;
+ }
+ const decoder = object({a: string(), b: constant(undefined)});
+
+ const run1 = decoder.run({a: 'qwerty', b: undefined});
+ expect(run1).toEqual({ok: true, result: {a: 'qwerty', b: undefined}});
+ expect(Result.map(Object.keys, run1)).toEqual({ok: true, result: ['a', 'b']});
+
+ const run2 = decoder.run({a: 'asdfgh'});
+ expect(run2).toEqual({ok: true, result: {a: 'asdfgh', b: undefined}});
+ expect(Result.map(Object.keys, run2)).toEqual({ok: true, result: ['a', 'b']});
+ });
+
it('can decode a constant array', () => {
type A = [1, 2, 3];
const decoder: Decoder = constant([1, 2, 3]);
@@ -179,8 +195,18 @@ describe('object', () => {
});
it('can decode a nested object', () => {
- const decoder = object({
- payload: object({x: number(), y: number()}),
+ interface Point {
+ x: number;
+ y: number;
+ }
+
+ interface Location {
+ payload: Point;
+ error: false;
+ }
+
+ const decoder = object({
+ payload: object({x: number(), y: number()}),
error: constant(false)
});
const json = {payload: {x: 5, y: 2}, error: false};
@@ -243,14 +269,53 @@ describe('object', () => {
});
});
- it('ignores optional fields that decode to undefined', () => {
- const decoder = object({
- a: number(),
- b: optional(string())
+ describe('optional and undefined fields', () => {
+ it('ignores optional fields that decode to undefined', () => {
+ interface AB {
+ a: number;
+ b?: string;
+ }
+
+ const decoder: Decoder = object({
+ a: number(),
+ b: optional(string())
+ });
+
+ expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}});
+ expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12}});
+ });
+
+ it('includes fields that are mapped to a value when not found', () => {
+ interface AB {
+ a: number;
+ b: string;
+ }
+
+ const decoder: Decoder = object({
+ a: number(),
+ b: oneOf(string(), constant(undefined)).map(
+ (b: string | undefined) => (b === undefined ? 'b not found' : b)
+ )
+ });
+
+ expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}});
+ expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12, b: 'b not found'}});
});
- expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}});
- expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12}});
+ it('includes fields that are mapped to a undefined when not found', () => {
+ interface AB {
+ a: number;
+ b: string | undefined;
+ }
+
+ const decoder: Decoder = object({
+ a: number(),
+ b: oneOf(string(), constant(undefined))
+ });
+
+ expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}});
+ expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12, b: undefined}});
+ });
});
});
@@ -334,32 +399,13 @@ describe('dict', () => {
});
describe('optional', () => {
- describe('decoding a non-object type', () => {
- const decoder = optional(number());
-
- it('can decode the given type', () => {
- expect(decoder.run(5)).toEqual({ok: true, result: 5});
- });
-
- it('can decode undefined', () => {
- expect(decoder.run(undefined)).toEqual({ok: true, result: undefined});
- });
-
- it('fails when the value is invalid', () => {
- expect(decoder.run(false)).toMatchObject({
- ok: false,
- error: {at: 'input', message: 'expected a number, got a boolean'}
- });
- });
- });
+ interface User {
+ id: number;
+ isDog?: boolean;
+ }
describe('decoding an interface with optional fields', () => {
- interface User {
- id: number;
- isDog?: boolean;
- }
-
- const decoder: Decoder = object({
+ const decoder = object({
id: number(),
isDog: optional(boolean())
});
@@ -380,6 +426,36 @@ describe('optional', () => {
});
});
});
+
+ it('supports map', () => {
+ const decoder = object({
+ id: number(),
+ isDog: optional(number()).map(num => num !== 0)
+ });
+
+ expect(decoder.run({id: 1, isDog: 0})).toEqual({ok: true, result: {id: 1, isDog: false}});
+ expect(decoder.run({id: 1, isDog: 77})).toEqual({ok: true, result: {id: 1, isDog: true}});
+ expect(decoder.run({id: 1})).toEqual({ok: true, result: {id: 1}});
+ });
+
+ it('supports andThen', () => {
+ const decoder = object({
+ id: number(),
+ isDog: optional(string()).andThen(
+ dogName =>
+ dogName.toLowerCase()[0] === 'd'
+ ? succeed(true)
+ : fail(`${dogName} is not a dog, all dog names start with 'D'`)
+ )
+ });
+
+ expect(decoder.run({id: 1, isDog: 'Doug'})).toEqual({ok: true, result: {id: 1, isDog: true}});
+ expect(decoder.run({id: 1, isDog: 'Wanda'})).toMatchObject({
+ ok: false,
+ error: {message: "Wanda is not a dog, all dog names start with 'D'"}
+ });
+ expect(decoder.run({id: 1})).toEqual({ok: true, result: {id: 1}});
+ });
});
describe('oneOf', () => {
@@ -449,8 +525,8 @@ describe('union', () => {
type C = A | B;
const decoder: Decoder = union(
- object({kind: constant<'a'>('a'), value: number()}),
- object({kind: constant<'b'>('b'), value: boolean()})
+ object({kind: constant<'a'>('a'), value: number()}),
+ object({kind: constant<'b'>('b'), value: boolean()})
);
it('can decode a value that matches one of the union types', () => {
@@ -485,7 +561,7 @@ describe('withDefault', () => {
});
describe('valueAt', () => {
- describe('decode an value', () => {
+ describe('decode a value accessed from a path', () => {
it('can decode a single object field', () => {
const decoder = valueAt(['a'], string());
expect(decoder.run({a: 'boots', b: 'cats'})).toEqual({ok: true, result: 'boots'});
@@ -527,7 +603,10 @@ describe('valueAt', () => {
});
describe('decode an optional field', () => {
- const decoder = valueAt(['a', 'b', 'c'], optional(string()));
+ const decoder: Decoder = valueAt(
+ ['a', 'b', 'c'],
+ union(string(), constant(undefined))
+ );
it('fails when the path does not exist', () => {
const error = decoder.run({a: {x: 'cats'}});
@@ -628,7 +707,7 @@ describe('lazy', () => {
replies: Comment[];
}
- const decoder: Decoder = object({
+ const decoder: Decoder = object({
msg: string(),
replies: lazy(() => array(decoder))
});
diff --git a/test/phone-example.test.ts b/test/phone-example.test.ts
index a179ca9..be554b8 100644
--- a/test/phone-example.test.ts
+++ b/test/phone-example.test.ts
@@ -42,14 +42,14 @@ describe('decode phone number objects', () => {
constant(PhoneUse.Work)
);
- const internationalPhoneDecoder: Decoder = object({
+ const internationalPhoneDecoder = object({
id: number(),
use: optional(phoneUseDecoder),
international: constant(true),
rawNumber: string()
});
- const domesticPhoneDecoder: Decoder = object({
+ const domesticPhoneDecoder = object({
id: number(),
use: optional(phoneUseDecoder),
international: constant(false),
@@ -58,7 +58,10 @@ describe('decode phone number objects', () => {
lineNumber: string()
});
- const phoneDecoder: Decoder = union(domesticPhoneDecoder, internationalPhoneDecoder);
+ const phoneDecoder: Decoder = union(
+ domesticPhoneDecoder,
+ internationalPhoneDecoder
+ );
const phonesDecoder: Decoder = array(phoneDecoder);
diff --git a/test/user-example.test.ts b/test/user-example.test.ts
index 9a620dc..fc7ab36 100644
--- a/test/user-example.test.ts
+++ b/test/user-example.test.ts
@@ -1,4 +1,4 @@
-import {Decoder, string, number, boolean, object} from '../src/index';
+import {string, number, boolean, object} from '../src/index';
describe('decode json as User interface', () => {
interface User {
@@ -22,7 +22,7 @@ describe('decode json as User interface', () => {
active: false
};
- const userDecoder: Decoder = object({
+ const userDecoder = object({
firstname: string(),
lastname: string(),
age: number(),
diff --git a/yarn.lock b/yarn.lock
index ea2fb28..853307a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -26,9 +26,9 @@
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
-"@types/fs-extra@5.0.1":
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.1.tgz#cd856fbbdd6af2c11f26f8928fd8644c9e9616c9"
+"@types/fs-extra@^5.0.3":
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.4.tgz#b971134d162cc0497d221adde3dbb67502225599"
dependencies:
"@types/node" "*"
@@ -40,29 +40,29 @@
"@types/minimatch" "*"
"@types/node" "*"
-"@types/handlebars@4.0.36":
- version "4.0.36"
- resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.36.tgz#ff57c77fa1ab6713bb446534ddc4d979707a3a79"
+"@types/handlebars@^4.0.38":
+ version "4.0.39"
+ resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd"
-"@types/highlight.js@9.12.2":
- version "9.12.2"
- resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.2.tgz#6ee7cd395effe5ec80b515d3ff1699068cd0cd1d"
+"@types/highlight.js@^9.12.3":
+ version "9.12.3"
+ resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca"
"@types/jest@^22.2.3":
version "22.2.3"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d"
-"@types/lodash@4.14.104":
- version "4.14.104"
- resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80"
-
"@types/lodash@^4.14.0":
version "4.14.114"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.114.tgz#e6f251af5994dd0d7ce141f9241439b4f40270f6"
-"@types/marked@0.3.0":
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
+"@types/lodash@^4.14.110":
+ version "4.14.116"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
+
+"@types/marked@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.4.0.tgz#057a6165703e7419217f8ffc6887747f980b6315"
"@types/minimatch@*", "@types/minimatch@3.0.3":
version "3.0.3"
@@ -72,9 +72,9 @@
version "10.5.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.3.tgz#5bcfaf088ad17894232012877669634c06b20cc5"
-"@types/shelljs@0.7.8":
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.8.tgz#4b4d6ee7926e58d7bca448a50ba442fd9f6715bd"
+"@types/shelljs@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.0.tgz#0caa56b68baae4f68f44e0dd666ab30b098e3632"
dependencies:
"@types/glob" "*"
"@types/node" "*"
@@ -1157,9 +1157,9 @@ fs-extra@^4.0.2:
jsonfile "^4.0.0"
universalify "^0.1.0"
-fs-extra@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
+fs-extra@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6"
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
@@ -2173,9 +2173,9 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
-marked@^0.3.17:
- version "0.3.19"
- resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
+marked@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66"
math-random@^1.0.1:
version "1.0.1"
@@ -2986,7 +2986,7 @@ shell-quote@^1.6.1:
array-reduce "~0.0.0"
jsonify "~0.0.0"
-shelljs@^0.8.1:
+shelljs@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35"
dependencies:
@@ -3420,31 +3420,31 @@ typedoc-plugin-markdown@^1.0.13:
dependencies:
"@forked/turndown" "^4.0.4"
-typedoc@^0.11.1:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.11.1.tgz#9f033887fd2218c769e1045feb88a1efed9f12c9"
+typedoc@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.12.0.tgz#c5d606f52af29d841658e18d9faa1a72acf0e270"
dependencies:
- "@types/fs-extra" "5.0.1"
- "@types/handlebars" "4.0.36"
- "@types/highlight.js" "9.12.2"
- "@types/lodash" "4.14.104"
- "@types/marked" "0.3.0"
+ "@types/fs-extra" "^5.0.3"
+ "@types/handlebars" "^4.0.38"
+ "@types/highlight.js" "^9.12.3"
+ "@types/lodash" "^4.14.110"
+ "@types/marked" "^0.4.0"
"@types/minimatch" "3.0.3"
- "@types/shelljs" "0.7.8"
- fs-extra "^5.0.0"
+ "@types/shelljs" "^0.8.0"
+ fs-extra "^7.0.0"
handlebars "^4.0.6"
highlight.js "^9.0.0"
- lodash "^4.17.5"
- marked "^0.3.17"
+ lodash "^4.17.10"
+ marked "^0.4.0"
minimatch "^3.0.0"
progress "^2.0.0"
- shelljs "^0.8.1"
+ shelljs "^0.8.2"
typedoc-default-themes "^0.5.0"
- typescript "2.7.2"
+ typescript "3.0.x"
-typescript@2.7.2:
- version "2.7.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836"
+typescript@3.0.x:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb"
typescript@~2.9.2:
version "2.9.2"