Skip to content

Commit

Permalink
Schema.optional: the default option now allows setting a default … (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored May 14, 2024
1 parent 89a3afb commit 992c8e2
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 39 deletions.
31 changes: 31 additions & 0 deletions .changeset/khaki-experts-run.md
Original file line number Diff line number Diff line change
@@ -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 }
```
30 changes: 30 additions & 0 deletions packages/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<A, I, R>`, `{ default: () => A }` | `PropertySignature<":", string, never, "?:", string \| undefined, never>` |
Expand Down
34 changes: 22 additions & 12 deletions packages/schema/dtslint/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 })) })

// ---------------------------------------------
Expand All @@ -690,47 +690,57 @@ 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 }
// ---------------------------------------------

// $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 })) })

// ---------------------------------------------
Expand Down
66 changes: 39 additions & 27 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1893,7 +1893,7 @@ export const optional: {
| I
| (Types.Has<Options, "nullable"> extends true ? null : never)
| (Types.Has<Options, "exact"> extends true ? never : undefined),
false,
Types.Has<Options, "default">,
R
>
<
Expand Down Expand Up @@ -1937,7 +1937,7 @@ export const optional: {
| I
| (Types.Has<Options, "nullable"> extends true ? null : never)
| (Types.Has<Options, "exact"> extends true ? never : undefined),
false,
Types.Has<Options, "default">,
R
>
} = dual((args) => isSchema(args[0]), <A, I, R>(
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 28 additions & 0 deletions packages/schema/test/Schema/optional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }", () => {
Expand Down Expand Up @@ -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 }", () => {
Expand Down Expand Up @@ -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 }", () => {
Expand Down Expand Up @@ -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 })
})
})
})

0 comments on commit 992c8e2

Please sign in to comment.