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/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..f626fb63 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -228,6 +228,14 @@ export const nullable: (or: Decoder) => Decoder /** * @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 */ export const fromStruct =

>>( @@ -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..c40a993c 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..4802f4da 100644 --- a/src/TaskDecoder.ts +++ b/src/TaskDecoder.ts @@ -231,6 +231,14 @@ export const nullable: (or: TaskDecoder) => TaskDecoder(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 */ export const fromStruct =

>>( @@ -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..2496ce14 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..a781bff1 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(undefined), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf(undefined, "undefined")))), + FS.of(DE.member(1, FS.of(DE.leaf(undefined, "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..d56833a7 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(undefined), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf(undefined, "undefined")))), + FS.of(DE.member(1, FS.of(DE.leaf(undefined, "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..0083ce73 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..5b4545c0 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/Schema.ts b/test/Schema.ts index 08890a6a..87a61178 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..7e8f843c 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(undefined)(), + E.left( + FS.concat( + FS.of(DE.member(0, FS.of(DE.leaf(undefined, "undefined")))), + FS.of(DE.member(1, FS.of(DE.leaf(undefined, "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..dc2325b0 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) =>