diff --git a/Decoder.md b/Decoder.md index e6b6589a..ff2d6da5 100644 --- a/Decoder.md +++ b/Decoder.md @@ -6,6 +6,7 @@ - [Combinators](#combinators) - [The `literal` constructor](#the-literal-constructor) - [The `nullable` combinator](#the-nullable-combinator) + - [The `optional` combinator](#the-optional-combinator) - [The `struct` combinator](#the-struct-combinator) - [The `partial` combinator](#the-partial-combinator) - [The `record` combinator](#the-record-combinator) @@ -100,6 +101,14 @@ The `nullable` combinator describes a nullable value export const NullableString: D.Decoder = D.nullable(D.string) ``` +## The `optional` combinator + +The `optional` combinator describes a optional value + +```ts +export const OptionalString: D.Decoder = D.optional(D.string) +``` + ## The `struct` combinator The `struct` combinator describes an object with required fields. diff --git a/Eq.md b/Eq.md index eaac1a43..2814f9c5 100644 --- a/Eq.md +++ b/Eq.md @@ -45,6 +45,7 @@ export const string: Eq = { - `literal` - `nullable` +- `optional` - `type` - `partial` - `record` diff --git a/docs/modules/Codec.ts.md b/docs/modules/Codec.ts.md index 841c0f3f..0d678841 100644 --- a/docs/modules/Codec.ts.md +++ b/docs/modules/Codec.ts.md @@ -34,6 +34,7 @@ Added in v2.2.3 - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) - [nullable](#nullable) + - [optional](#optional) - [partial](#partial) - [readonly](#readonly) - [record](#record) @@ -226,6 +227,16 @@ export declare function nullable(or: Codec): Codec(or: Codec): Codec +``` + +Added in v2.3.0 + ## partial **Signature** diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md index e2b947d7..b8fa6d0c 100644 --- a/docs/modules/Decoder.ts.md +++ b/docs/modules/Decoder.ts.md @@ -44,6 +44,7 @@ Added in v2.2.7 - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) - [nullable](#nullable) + - [optional](#optional) - [parse](#parse) - [partial](#partial) - [readonly](#readonly) @@ -307,6 +308,16 @@ export declare const nullable: (or: Decoder) => Decoder Added in v2.2.7 +## optional + +**Signature** + +```ts +export declare const optional: (or: Decoder) => Decoder +``` + +Added in v2.3.0 + ## parse **Signature** diff --git a/docs/modules/Encoder.ts.md b/docs/modules/Encoder.ts.md index 153decb6..21167bf7 100644 --- a/docs/modules/Encoder.ts.md +++ b/docs/modules/Encoder.ts.md @@ -30,6 +30,7 @@ Added in v2.2.3 - [intersect](#intersect) - [lazy](#lazy) - [nullable](#nullable) + - [optional](#optional) - [partial](#partial) - [readonly](#readonly) - [record](#record) @@ -128,6 +129,16 @@ export declare function nullable(or: Encoder): Encoder(or: Encoder): Encoder +``` + +Added in v2.3.0 + ## partial **Signature** diff --git a/docs/modules/Eq.ts.md b/docs/modules/Eq.ts.md index 5043ed51..1b8c6791 100644 --- a/docs/modules/Eq.ts.md +++ b/docs/modules/Eq.ts.md @@ -24,6 +24,7 @@ Added in v2.2.2 - [intersect](#intersect) - [lazy](#lazy) - [nullable](#nullable) + - [optional](#optional) - [partial](#partial) - [readonly](#readonly) - [record](#record) @@ -89,6 +90,16 @@ export declare function nullable(or: Eq): Eq Added in v2.2.2 +## optional + +**Signature** + +```ts +export declare function optional(or: Eq): Eq +``` + +Added in v2.3.0 + ## partial **Signature** diff --git a/docs/modules/Guard.ts.md b/docs/modules/Guard.ts.md index ce797e48..3b60c4d9 100644 --- a/docs/modules/Guard.ts.md +++ b/docs/modules/Guard.ts.md @@ -27,6 +27,7 @@ Added in v2.2.0 - [intersect](#intersect) - [lazy](#lazy) - [nullable](#nullable) + - [optional](#optional) - [partial](#partial) - [readonly](#readonly) - [record](#record) @@ -132,6 +133,16 @@ export declare const nullable: (or: Guard) => Guard Added in v2.2.0 +## optional + +**Signature** + +```ts +export declare const optional: (or: Guard) => Guard +``` + +Added in v2.3.0 + ## partial **Signature** diff --git a/docs/modules/Kleisli.ts.md b/docs/modules/Kleisli.ts.md index d29e5496..161b58ee 100644 --- a/docs/modules/Kleisli.ts.md +++ b/docs/modules/Kleisli.ts.md @@ -34,6 +34,7 @@ Added in v2.2.7 - [map](#map) - [mapLeftWithInput](#mapleftwithinput) - [nullable](#nullable) + - [optional](#optional) - [parse](#parse) - [refine](#refine) - [union](#union) @@ -237,6 +238,18 @@ export declare function nullable( Added in v2.2.7 +## optional + +**Signature** + +```ts +export declare function optional( + M: Applicative2C & Bifunctor2 +): (onError: (i: I, e: E) => E) => (or: Kleisli) => Kleisli +``` + +Added in v2.3.0 + ## parse **Signature** diff --git a/docs/modules/Schemable.ts.md b/docs/modules/Schemable.ts.md index 0d146264..91b9f5dc 100644 --- a/docs/modules/Schemable.ts.md +++ b/docs/modules/Schemable.ts.md @@ -63,6 +63,7 @@ export interface Schemable { readonly number: HKT readonly boolean: HKT readonly nullable: (or: HKT) => HKT + readonly optional: (or: HKT) => HKT /** @deprecated */ readonly type: (properties: { [K in keyof A]: HKT }) => HKT readonly struct: (properties: { [K in keyof A]: HKT }) => HKT @@ -95,6 +96,7 @@ export interface Schemable1 { readonly number: Kind readonly boolean: Kind readonly nullable: (or: Kind) => Kind + readonly optional: (or: Kind) => Kind /** @deprecated */ readonly type: (properties: { [K in keyof A]: Kind }) => Kind readonly struct: (properties: { [K in keyof A]: Kind }) => Kind @@ -127,6 +129,7 @@ export interface Schemable2C { readonly number: Kind2 readonly boolean: Kind2 readonly nullable: (or: Kind2) => Kind2 + readonly optional: (or: Kind2) => Kind2 /** @deprecated */ readonly type: (properties: { [K in keyof A]: Kind2 }) => Kind2 readonly struct: (properties: { [K in keyof A]: Kind2 }) => Kind2 diff --git a/docs/modules/TaskDecoder.ts.md b/docs/modules/TaskDecoder.ts.md index ae2cd3e0..842d8557 100644 --- a/docs/modules/TaskDecoder.ts.md +++ b/docs/modules/TaskDecoder.ts.md @@ -44,6 +44,7 @@ Added in v2.2.7 - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) - [nullable](#nullable) + - [optional](#optional) - [parse](#parse) - [partial](#partial) - [readonly](#readonly) @@ -310,6 +311,16 @@ export declare const nullable: (or: TaskDecoder) => TaskDecoder(or: TaskDecoder) => TaskDecoder +``` + +Added in v2.3.0 + ## parse **Signature** diff --git a/docs/modules/Type.ts.md b/docs/modules/Type.ts.md index c6ceb033..efb89778 100644 --- a/docs/modules/Type.ts.md +++ b/docs/modules/Type.ts.md @@ -24,6 +24,7 @@ Added in v2.2.3 - [intersect](#intersect) - [lazy](#lazy) - [nullable](#nullable) + - [optional](#optional) - [partial](#partial) - [readonly](#readonly) - [record](#record) @@ -95,6 +96,16 @@ export declare const nullable: (or: Type) => Type Added in v2.2.3 +## optional + +**Signature** + +```ts +export declare const optional: (or: Type) => Type +``` + +Added in v2.3.0 + ## partial **Signature** diff --git a/dtslint/ts3.5/Encoder.ts b/dtslint/ts3.5/Encoder.ts index 8bb901d4..d5911e13 100644 --- a/dtslint/ts3.5/Encoder.ts +++ b/dtslint/ts3.5/Encoder.ts @@ -26,6 +26,11 @@ export type OfTestOutput = E.OutputOf // $ExpectType { a: string; // E.nullable(NumberToString) // $ExpectType Encoder +// +// optional +// +E.optional(NumberToString) // $ExpectType Encoder + // // struct // diff --git a/dtslint/ts3.5/Schema.ts b/dtslint/ts3.5/Schema.ts index ee8729eb..da570ac7 100644 --- a/dtslint/ts3.5/Schema.ts +++ b/dtslint/ts3.5/Schema.ts @@ -53,6 +53,12 @@ make((S) => S.boolean) // $ExpectType Schema make((S) => S.nullable(S.string)) // $ExpectType Schema +// +// optional +// + +make((S) => S.optional(S.string)) // $ExpectType Schema + // // struct // diff --git a/package-lock.json b/package-lock.json index 5234b1b7..8ac4493c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "io-ts", - "version": "2.2.17", + "version": "2.2.20", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "io-ts", - "version": "2.2.17", + "version": "2.2.20", "license": "MIT", "devDependencies": { "@types/benchmark": "1.0.31", @@ -13462,7 +13462,7 @@ "dtslint": { "version": "git+ssh://git@github.com/gcanti/dtslint.git#f361dc93d6a195f530df28779082548e01cecd5e", "dev": true, - "from": "dtslint@gcanti/dtslint", + "from": "dtslint@github:gcanti/dtslint", "requires": { "fs-extra": "^6.0.1", "parsimmon": "^1.12.0", diff --git a/src/Codec.ts b/src/Codec.ts index 9465e02e..7d589b0d 100644 --- a/src/Codec.ts +++ b/src/Codec.ts @@ -142,6 +142,14 @@ export function nullable(or: Codec): Codec(or: Codec): Codec { + return make(D.optional(or), E.optional(or)) +} + /** * @category combinators * @since 2.2.15 diff --git a/src/Decoder.ts b/src/Decoder.ts index 702cc9bb..d87a4d34 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -226,6 +226,14 @@ export const nullable: (or: Decoder) => Decoder /*#__PURE__*/ K.nullable(M)((u, e) => FS.concat(FS.of(DE.member(0, error(u, 'null'))), FS.of(DE.member(1, e)))) +/** + * @category combinators + * @since 2.3.0 + */ +export const optional: (or: Decoder) => Decoder = + /*#__PURE__*/ + K.optional(M)((u, e) => FS.concat(FS.of(DE.member(0, error(u, 'undefined'))), FS.of(DE.member(1, e)))) + /** * @category combinators * @since 2.2.15 @@ -488,6 +496,7 @@ export const Schemable: S.Schemable2C = { number, boolean, nullable, + optional, type, struct, partial, diff --git a/src/Encoder.ts b/src/Encoder.ts index 8cf7a085..2dc41141 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -39,6 +39,16 @@ export function nullable(or: Encoder): Encoder { } } +/** + * @category combinators + * @since 2.3.0 + */ +export function optional(or: Encoder): Encoder { + return { + encode: (a) => (a === undefined ? undefined : or.encode(a)) + } +} + /** * @category combinators * @since 2.2.15 diff --git a/src/Eq.ts b/src/Eq.ts index 67015156..c8062775 100644 --- a/src/Eq.ts +++ b/src/Eq.ts @@ -89,6 +89,16 @@ export function nullable(or: Eq): Eq { } } +/** + * @category combinators + * @since 2.3.0 + */ +export function optional(or: Eq): Eq { + return { + equals: (x, y) => (x === undefined || y === undefined ? x === y : or.equals(x, y)) + } +} + /** * @category combinators * @since 2.2.15 @@ -204,6 +214,7 @@ export const Schemable: Schemable1 = { number, boolean, nullable, + optional, type, struct, partial, diff --git a/src/Guard.ts b/src/Guard.ts index d189660f..741adc63 100644 --- a/src/Guard.ts +++ b/src/Guard.ts @@ -120,6 +120,14 @@ export const nullable = (or: Guard): Guard i === null || or.is(i) }) +/** + * @category combinators + * @since 2.3.0 + */ +export const optional = (or: Guard): Guard => ({ + is: (i): i is undefined | A => i === undefined || or.is(i) +}) + /** * @category combinators * @since 2.2.15 @@ -325,6 +333,7 @@ export const Schemable: S.Schemable1 = { number, boolean, nullable, + optional, type, struct, partial, diff --git a/src/Kleisli.ts b/src/Kleisli.ts index b6cba277..6d9ec2c8 100644 --- a/src/Kleisli.ts +++ b/src/Kleisli.ts @@ -123,6 +123,28 @@ export function nullable( }) } +/** + * @category combinators + * @since 2.3.0 + */ +export function optional( + M: Applicative2C & Bifunctor2 +): ( + onError: (i: I, e: E) => E +) => (or: Kleisli) => Kleisli { + return (onError: (i: I, e: E) => E) => + (or: Kleisli): Kleisli => ({ + decode: (i) => + i === undefined + ? M.of(undefined) + : M.bimap( + or.decode(i), + (e) => onError(i, e), + (a): A | undefined => a + ) + }) +} + /** * @category combinators * @since 2.2.15 diff --git a/src/Schemable.ts b/src/Schemable.ts index 1eda7c3c..5a654537 100644 --- a/src/Schemable.ts +++ b/src/Schemable.ts @@ -28,6 +28,7 @@ export interface Schemable { readonly number: HKT readonly boolean: HKT readonly nullable: (or: HKT) => HKT + readonly optional: (or: HKT) => HKT /** @deprecated */ readonly type: (properties: { [K in keyof A]: HKT }) => HKT readonly struct: (properties: { [K in keyof A]: HKT }) => HKT @@ -55,6 +56,7 @@ export interface Schemable1 { readonly number: Kind readonly boolean: Kind readonly nullable: (or: Kind) => Kind + readonly optional: (or: Kind) => Kind /** @deprecated */ readonly type: (properties: { [K in keyof A]: Kind }) => Kind readonly struct: (properties: { [K in keyof A]: Kind }) => Kind @@ -82,6 +84,7 @@ export interface Schemable2C { readonly number: Kind2 readonly boolean: Kind2 readonly nullable: (or: Kind2) => Kind2 + readonly optional: (or: Kind2) => Kind2 /** @deprecated */ readonly type: (properties: { [K in keyof A]: Kind2 }) => Kind2 readonly struct: (properties: { [K in keyof A]: Kind2 }) => Kind2 diff --git a/src/TaskDecoder.ts b/src/TaskDecoder.ts index 0af1501a..3ed2664e 100644 --- a/src/TaskDecoder.ts +++ b/src/TaskDecoder.ts @@ -229,6 +229,14 @@ export const nullable: (or: TaskDecoder) => TaskDecoder FS.concat(FS.of(DE.member(0, error(u, 'null'))), FS.of(DE.member(1, e)))) +/** + * @category combinators + * @since 2.2.7 + */ +export const optional: (or: TaskDecoder) => TaskDecoder = + /*#__PURE__*/ + K.optional(M)((u, e) => FS.concat(FS.of(DE.member(0, error(u, 'undefined'))), FS.of(DE.member(1, e)))) + /** * @category combinators * @since 2.2.15 @@ -494,6 +502,7 @@ export const Schemable: S.Schemable2C = { number, boolean, nullable, + optional, type, struct, partial, diff --git a/src/Type.ts b/src/Type.ts index 26ab3f20..eded9dd4 100644 --- a/src/Type.ts +++ b/src/Type.ts @@ -97,6 +97,12 @@ export const refine = (refinement: Refinement, id: string) */ export const nullable = (or: Type): Type => t.union([t.null, or]) +/** + * @category combinators + * @since 2.3.0 + */ +export const optional = (or: Type): Type => t.union([t.undefined, or]) + /** * @category combinators * @since 2.2.15 @@ -206,6 +212,7 @@ export const Schemable: S.Schemable1 = { number, boolean, nullable, + optional, type, struct, partial, diff --git a/test/Arbitrary.ts b/test/Arbitrary.ts index 0eccbfa9..0cdf9e22 100644 --- a/test/Arbitrary.ts +++ b/test/Arbitrary.ts @@ -52,6 +52,10 @@ export function nullable(or: Arbitrary): Arbitrary { return fc.oneof(fc.constant(null), or) } +export function optional(or: Arbitrary): Arbitrary { + return fc.oneof(fc.constant(undefined), or) +} + export function struct(properties: { [K in keyof A]: Arbitrary }): Arbitrary { return fc.record(properties) } @@ -125,6 +129,7 @@ export const Schemable: S.Schemable1 & S.WithUnknownContainers1 & S.Wi number, boolean, nullable, + optional, type: struct, struct, partial, diff --git a/test/Codec.ts b/test/Codec.ts index 8b228ee3..d87390c5 100644 --- a/test/Codec.ts +++ b/test/Codec.ts @@ -205,6 +205,46 @@ describe('Codec', () => { }) }) + describe('optional', () => { + describe('decode', () => { + it('should decode a valid input', () => { + const codec = _.optional(codecNumber) + assert.deepStrictEqual(codec.decode(undefined), D.success(undefined)) + assert.deepStrictEqual(codec.decode('1'), D.success(1)) + }) + + it('should reject an invalid input', () => { + const codec = _.optional(codecNumber) + assert.deepStrictEqual( + codec.decode(null), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf(null, 'undefined')))), + FS.of(DE.member(1, FS.of(DE.leaf(null, 'string')))) + ) + ) + ) + assert.deepStrictEqual( + codec.decode('a'), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf('a', 'undefined')))), + FS.of(DE.member(1, FS.of(DE.leaf('a', 'parsable to a number')))) + ) + ) + ) + }) + }) + + describe('encode', () => { + it('should encode a value', () => { + const codec = _.optional(codecNumber) + assert.strictEqual(codec.encode(undefined), undefined) + assert.strictEqual(codec.encode(1), '1') + }) + }) + }) + describe('struct', () => { describe('decode', () => { it('should decode a valid input', () => { diff --git a/test/Decoder.ts b/test/Decoder.ts index a7e82449..b91120a7 100644 --- a/test/Decoder.ts +++ b/test/Decoder.ts @@ -153,6 +153,36 @@ describe('Decoder', () => { }) }) + describe('optional', () => { + it('should decode a valid input', () => { + const decoder = _.optional(H.decoderNumberFromUnknownString) + assert.deepStrictEqual(decoder.decode(undefined), _.success(undefined)) + assert.deepStrictEqual(decoder.decode('1'), _.success(1)) + }) + + it('should reject an invalid input', () => { + const decoder = _.optional(H.decoderNumberFromUnknownString) + assert.deepStrictEqual( + decoder.decode(null), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf(null, 'undefined')))), + FS.of(DE.member(1, FS.of(DE.leaf(null, 'string')))) + ) + ) + ) + assert.deepStrictEqual( + decoder.decode('a'), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf('a', 'undefined')))), + FS.of(DE.member(1, FS.of(DE.leaf('a', 'parsable to a number')))) + ) + ) + ) + }) + }) + describe('struct', () => { it('should decode a valid input', async () => { const decoder = _.struct({ diff --git a/test/Encoder.ts b/test/Encoder.ts index d4155a63..994624fd 100644 --- a/test/Encoder.ts +++ b/test/Encoder.ts @@ -20,6 +20,12 @@ describe('Encoder', () => { assert.deepStrictEqual(encoder.encode(null), null) }) + it('optional', () => { + const encoder = E.optional(H.encoderNumberToString) + assert.deepStrictEqual(encoder.encode(1), '1') + assert.deepStrictEqual(encoder.encode(undefined), undefined) + }) + it('struct', () => { const encoder = E.struct({ a: H.encoderNumberToString, b: H.encoderBooleanToNumber }) assert.deepStrictEqual(encoder.encode({ a: 1, b: true }), { a: '1', b: 1 }) diff --git a/test/Guard.ts b/test/Guard.ts index 23cd4318..158d6e81 100644 --- a/test/Guard.ts +++ b/test/Guard.ts @@ -79,6 +79,19 @@ describe('Guard', () => { }) }) + describe('optional', () => { + it('should accept valid inputs', () => { + const guard = G.optional(G.string) + assert.strictEqual(guard.is(undefined), true) + assert.strictEqual(guard.is('a'), true) + }) + + it('should reject invalid inputs', () => { + const guard = G.optional(G.string) + assert.strictEqual(guard.is(1), false) + }) + }) + describe('struct', () => { it('should accept valid inputs', () => { const guard = G.struct({ a: G.string, b: G.number }) diff --git a/test/JsonSchema.ts b/test/JsonSchema.ts index 7a1e8e3e..826f7102 100644 --- a/test/JsonSchema.ts +++ b/test/JsonSchema.ts @@ -58,10 +58,18 @@ const nullJsonSchema: JsonSchema = { compile: () => C.make({ enum: [null] }) } +const undefinedJsonSchema: JsonSchema = { + compile: () => C.make({ enum: [] }) +} + export function nullable(or: JsonSchema): JsonSchema { return union(nullJsonSchema, or) } +export function optional(or: JsonSchema): JsonSchema { + return union(undefinedJsonSchema, or) +} + export function struct(properties: { [K in keyof A]: JsonSchema }): JsonSchema { return { compile: (lazy) => @@ -194,6 +202,7 @@ export const Schemable: S.Schemable1 & S.WithUnknownContainers1 & S.Wi UnknownArray, UnknownRecord, nullable, + optional, type: struct, struct, partial, diff --git a/test/Schema.ts b/test/Schema.ts index 08890a6a..9da3766e 100644 --- a/test/Schema.ts +++ b/test/Schema.ts @@ -38,6 +38,10 @@ describe('Schema', () => { check(make((S) => S.nullable(S.string))) }) + it('optional', () => { + check(make((S) => S.optional(S.string))) + }) + it('struct', () => { check( make((S) => diff --git a/test/TaskDecoder.ts b/test/TaskDecoder.ts index a944df23..aa240523 100644 --- a/test/TaskDecoder.ts +++ b/test/TaskDecoder.ts @@ -196,6 +196,36 @@ describe('UnknownTaskDecoder', () => { }) }) + describe('optional', () => { + it('should decode a valid input', async () => { + const decoder = _.optional(NumberFromString) + assert.deepStrictEqual(await decoder.decode(undefined)(), D.success(undefined)) + assert.deepStrictEqual(await decoder.decode('1')(), D.success(1)) + }) + + it('should reject an invalid input', async () => { + const decoder = _.optional(NumberFromString) + assert.deepStrictEqual( + await decoder.decode(null)(), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf(null, 'undefined')))), + FS.of(DE.member(1, FS.of(DE.leaf(null, 'string')))) + ) + ) + ) + assert.deepStrictEqual( + await decoder.decode('a')(), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf('a', 'undefined')))), + FS.of(DE.member(1, FS.of(DE.leaf('a', 'parsable to a number')))) + ) + ) + ) + }) + }) + describe('struct', () => { it('should decode a valid input', async () => { const decoder = _.struct({ diff --git a/test/Type.ts b/test/Type.ts index 567fe097..43e9bd9c 100644 --- a/test/Type.ts +++ b/test/Type.ts @@ -105,6 +105,13 @@ describe('Type', () => { ) }) + it('optional', () => { + check( + make((S) => S.optional(S.string)), + t.union([t.undefined, t.string]) + ) + }) + it('struct', () => { check( make((S) =>