From 992c8e21535db9f0c66e81d32fee8af56a96274f Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Tue, 14 May 2024 08:39:12 +0200 Subject: [PATCH] =?UTF-8?q?`Schema.optional`:=20the=20`default`=20option?= =?UTF-8?q?=20now=20allows=20setting=20a=20default=20=E2=80=A6=20(#2741)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/khaki-experts-run.md | 31 +++++++++ packages/schema/README.md | 30 +++++++++ packages/schema/dtslint/Schema.ts | 34 ++++++---- packages/schema/src/Schema.ts | 66 ++++++++++++-------- packages/schema/test/Schema/optional.test.ts | 28 +++++++++ 5 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 .changeset/khaki-experts-run.md diff --git a/.changeset/khaki-experts-run.md b/.changeset/khaki-experts-run.md new file mode 100644 index 0000000000..855d19a0f3 --- /dev/null +++ b/.changeset/khaki-experts-run.md @@ -0,0 +1,31 @@ +--- +"@effect/schema": patch +--- + +`Schema.optional`: the `default` option now allows setting a default value for **both** the decoding phase and the default constructor. Previously, it only set the decoding default. Closes #2740. + +**Example** + +```ts +import { Schema } from "@effect/schema" + +const Product = Schema.Struct({ + name: Schema.String, + price: Schema.NumberFromString, + quantity: Schema.optional(Schema.NumberFromString, { default: () => 1 }) +}) + +// Applying defaults in the decoding phase +console.log(Schema.decodeUnknownSync(Product)({ name: "Laptop", price: "999" })) // { name: 'Laptop', price: 999, quantity: 1 } +console.log( + Schema.decodeUnknownSync(Product)({ + name: "Laptop", + price: "999", + quantity: "2" + }) +) // { name: 'Laptop', price: 999, quantity: 2 } + +// Applying defaults in the constructor +console.log(Product.make({ name: "Laptop", price: 999 })) // { name: 'Laptop', price: 999, quantity: 1 } +console.log(Product.make({ name: "Laptop", price: 999, quantity: 2 })) // { name: 'Laptop', price: 999, quantity: 2 } +``` diff --git a/packages/schema/README.md b/packages/schema/README.md index 2ea7eb16b0..39c991d749 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -2622,6 +2622,36 @@ console.log(Schema.decodeUnknownSync(Person)({ name: "name", AGE: "18" })) ### Default Values +The `default` option allows you to set a default value for both the decoding phase and the default constructor. + +**Example** + +Let's see how default values work in both the decoding and constructing phases, illustrating how the default value is applied when certain properties are not provided. + +```ts +import { Schema } from "@effect/schema" + +const Product = Schema.Struct({ + name: Schema.String, + price: Schema.NumberFromString, + quantity: Schema.optional(Schema.NumberFromString, { default: () => 1 }) +}) + +// Applying defaults in the decoding phase +console.log(Schema.decodeUnknownSync(Product)({ name: "Laptop", price: "999" })) // { name: 'Laptop', price: 999, quantity: 1 } +console.log( + Schema.decodeUnknownSync(Product)({ + name: "Laptop", + price: "999", + quantity: "2" + }) +) // { name: 'Laptop', price: 999, quantity: 2 } + +// Applying defaults in the constructor +console.log(Product.make({ name: "Laptop", price: 999 })) // { name: 'Laptop', price: 999, quantity: 1 } +console.log(Product.make({ name: "Laptop", price: 999, quantity: 2 })) // { name: 'Laptop', price: 999, quantity: 2 } +``` + | Combinator | From | To | | ---------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `optional` | `Schema`, `{ default: () => A }` | `PropertySignature<":", string, never, "?:", string \| undefined, never>` | diff --git a/packages/schema/dtslint/Schema.ts b/packages/schema/dtslint/Schema.ts index 4a0a81618f..9da4d7aea8 100644 --- a/packages/schema/dtslint/Schema.ts +++ b/packages/schema/dtslint/Schema.ts @@ -653,7 +653,7 @@ S.asSchema(S.Struct({ c: S.optional(S.Boolean, { exact: true, default: () => false }) })) -// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", boolean, never, "?:", boolean, false, never>; }> +// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", boolean, never, "?:", boolean, true, never>; }> S.Struct({ a: S.String, b: S.Number, @@ -667,20 +667,20 @@ S.asSchema(S.Struct({ c: S.optional(S.NumberFromString, { exact: true, default: () => 0 }) })) -// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", number, never, "?:", string, false, never>; }> +// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", number, never, "?:", string, true, never>; }> S.Struct({ a: S.String, b: S.Number, c: S.optional(S.NumberFromString, { exact: true, default: () => 0 }) }) -// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b", false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b", true, never>; }> S.Struct({ a: S.optional(S.Literal("a", "b"), { default: () => "a", exact: true }) }) // $ExpectType Schema<{ readonly a: "a" | "b"; }, { readonly a?: "a" | "b"; }, never> S.asSchema(S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const, exact: true })) })) -// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b", false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b", true, never>; }> S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const, exact: true })) }) // --------------------------------------------- @@ -690,24 +690,34 @@ S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const, // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c: boolean; }, { readonly a: string; readonly b: number; readonly c?: boolean | undefined; }, never> S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.Boolean, { default: () => false }) })) -// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", boolean, never, "?:", boolean | undefined, false, never>; }> +// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", boolean, never, "?:", boolean | undefined, true, never>; }> S.Struct({ a: S.String, b: S.Number, c: S.optional(S.Boolean, { default: () => false }) }) // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c: number; }, { readonly a: string; readonly b: number; readonly c?: string | undefined; }, never> S.asSchema(S.Struct({ a: S.String, b: S.Number, c: S.optional(S.NumberFromString, { default: () => 0 }) })) -// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", number, never, "?:", string | undefined, false, never>; }> +// $ExpectType Struct<{ a: typeof String$; b: typeof Number$; c: PropertySignature<":", number, never, "?:", string | undefined, true, never>; }> S.Struct({ a: S.String, b: S.Number, c: S.optional(S.NumberFromString, { default: () => 0 }) }) -// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | undefined, false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | undefined, true, never>; }> S.Struct({ a: S.optional(S.Literal("a", "b"), { default: () => "a" }) }) // $ExpectType Schema<{ readonly a: "a" | "b"; }, { readonly a?: "a" | "b" | undefined; }, never> S.asSchema(S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const })) })) -// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | undefined, false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | undefined, true, never>; }> S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const })) }) +// --------------------------------------------- +// optional { exact: true, nullable: true, default: () => A } +// --------------------------------------------- + +// $ExpectType Schema<{ readonly a: number; }, { readonly a?: string | null; }, never> +S.asSchema(S.Struct({ a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) })) + +// $ExpectType Struct<{ a: PropertySignature<":", number, never, "?:", string | null, true, never>; }> +S.Struct({ a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) + // --------------------------------------------- // optional { nullable: true, default: () => A } // --------------------------------------------- @@ -715,22 +725,22 @@ S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const // $ExpectType Schema<{ readonly a: number; }, { readonly a?: string | null | undefined; }, never> S.asSchema(S.Struct({ a: S.optional(S.NumberFromString, { nullable: true, default: () => 0 }) })) -// $ExpectType Struct<{ a: PropertySignature<":", number, never, "?:", string | null | undefined, false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", number, never, "?:", string | null | undefined, true, never>; }> S.Struct({ a: S.optional(S.NumberFromString, { nullable: true, default: () => 0 }) }) // $ExpectType Schema<{ readonly a: number; }, { readonly a?: string | null; }, never> S.asSchema(S.Struct({ a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) })) -// $ExpectType Struct<{ a: PropertySignature<":", number, never, "?:", string | null, false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", number, never, "?:", string | null, true, never>; }> S.Struct({ a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) -// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | null | undefined, false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | null | undefined, true, never>; }> S.Struct({ a: S.optional(S.Literal("a", "b"), { default: () => "a", nullable: true }) }) // $ExpectType Schema<{ readonly a: "a" | "b"; }, { readonly a?: "a" | "b" | null | undefined; }, never> S.asSchema(S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const, nullable: true })) })) -// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | null | undefined, false, never>; }> +// $ExpectType Struct<{ a: PropertySignature<":", "a" | "b", never, "?:", "a" | "b" | null | undefined, true, never>; }> S.Struct({ a: S.Literal("a", "b").pipe(S.optional({ default: () => "a" as const, nullable: true })) }) // --------------------------------------------- diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 72d3e4d9c7..68c283cecd 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -1893,7 +1893,7 @@ export const optional: { | I | (Types.Has extends true ? null : never) | (Types.Has extends true ? never : undefined), - false, + Types.Has, R > < @@ -1937,7 +1937,7 @@ export const optional: { | I | (Types.Has extends true ? null : never) | (Types.Has extends true ? never : undefined), - false, + Types.Has, R > } = dual((args) => isSchema(args[0]), ( @@ -1957,19 +1957,25 @@ export const optional: { if (isExact) { if (defaultValue) { if (isNullable) { - return optionalToRequired( - NullOr(schema), - typeSchema(schema), - { - decode: option_.match({ onNone: defaultValue, onSome: (a) => a === null ? defaultValue() : a }), - encode: option_.some - } + return withConstructorDefault( + optionalToRequired( + NullOr(schema), + typeSchema(schema), + { + decode: option_.match({ onNone: defaultValue, onSome: (a) => a === null ? defaultValue() : a }), + encode: option_.some + } + ), + defaultValue ) } else { - return optionalToRequired( - schema, - typeSchema(schema), - { decode: option_.match({ onNone: defaultValue, onSome: identity }), encode: option_.some } + return withConstructorDefault( + optionalToRequired( + schema, + typeSchema(schema), + { decode: option_.match({ onNone: defaultValue, onSome: identity }), encode: option_.some } + ), + defaultValue ) } } else if (asOption) { @@ -2000,22 +2006,28 @@ export const optional: { } else { if (defaultValue) { if (isNullable) { - return optionalToRequired( - NullishOr(schema), - typeSchema(schema), - { - decode: option_.match({ onNone: defaultValue, onSome: (a) => (a == null ? defaultValue() : a) }), - encode: option_.some - } + return withConstructorDefault( + optionalToRequired( + NullishOr(schema), + typeSchema(schema), + { + decode: option_.match({ onNone: defaultValue, onSome: (a) => (a == null ? defaultValue() : a) }), + encode: option_.some + } + ), + defaultValue ) } else { - return optionalToRequired( - UndefinedOr(schema), - typeSchema(schema), - { - decode: option_.match({ onNone: defaultValue, onSome: (a) => (a === undefined ? defaultValue() : a) }), - encode: option_.some - } + return withConstructorDefault( + optionalToRequired( + UndefinedOr(schema), + typeSchema(schema), + { + decode: option_.match({ onNone: defaultValue, onSome: (a) => (a === undefined ? defaultValue() : a) }), + encode: option_.some + } + ), + defaultValue ) } } else if (asOption) { diff --git a/packages/schema/test/Schema/optional.test.ts b/packages/schema/test/Schema/optional.test.ts index 07ebf1fdb4..0885e49504 100644 --- a/packages/schema/test/Schema/optional.test.ts +++ b/packages/schema/test/Schema/optional.test.ts @@ -297,6 +297,13 @@ describe("optional APIs", () => { await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { exact: true, default: () => 0 }) + }) + expect(schema.make({})).toStrictEqual({ a: 0 }) + }) }) describe("optional > { default: () => A }", () => { @@ -326,6 +333,13 @@ describe("optional APIs", () => { await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { default: () => 0 }) + }) + expect(schema.make({})).toStrictEqual({ a: 0 }) + }) }) describe("optional > { nullable: true, default: () => A }", () => { @@ -358,6 +372,13 @@ describe("optional APIs", () => { await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { nullable: true, default: () => 0 }) + }) + expect(schema.make({})).toStrictEqual({ a: 0 }) + }) }) describe("optional > { exact: true, nullable: true, default: () => A }", () => { @@ -387,5 +408,12 @@ describe("optional APIs", () => { await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) + + it("should apply the default to the default constructor", () => { + const schema = S.Struct({ + a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) + }) + expect(schema.make({})).toStrictEqual({ a: 0 }) + }) }) })