diff --git a/.changeset/chilled-jars-develop.md b/.changeset/chilled-jars-develop.md new file mode 100644 index 000000000..97f28688b --- /dev/null +++ b/.changeset/chilled-jars-develop.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": minor +--- + +Arbitrary: should throw on declarations without annotations diff --git a/.changeset/dirty-taxis-enjoy.md b/.changeset/dirty-taxis-enjoy.md new file mode 100644 index 000000000..a9d1a12d3 --- /dev/null +++ b/.changeset/dirty-taxis-enjoy.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Schema: fix declarations (`type` field) diff --git a/.changeset/four-suits-fetch.md b/.changeset/four-suits-fetch.md new file mode 100644 index 000000000..f870b33ee --- /dev/null +++ b/.changeset/four-suits-fetch.md @@ -0,0 +1,11 @@ +--- +"@effect/schema": minor +--- + +Schema: refactor `S.optional` API. + +Upgrade Guide: + +- `S.optional(schema, { exact: true })` replaces the old `S.optional(schema)` +- `S.optional(schema, { exact: true, default: () => A })` replaces the old `S.optional(schema).withDefault(() => A)` +- `S.optional(schema, { exact: true, as: "Option" })` replaces the old `S.optional(schema).toOption()` diff --git a/.changeset/fuzzy-waves-raise.md b/.changeset/fuzzy-waves-raise.md new file mode 100644 index 000000000..142e3b7dd --- /dev/null +++ b/.changeset/fuzzy-waves-raise.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Schema: add `nullish` diff --git a/.changeset/silent-paws-move.md b/.changeset/silent-paws-move.md new file mode 100644 index 000000000..c65545ad4 --- /dev/null +++ b/.changeset/silent-paws-move.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Schema: add `orUndefined` diff --git a/.changeset/six-maps-jog.md b/.changeset/six-maps-jog.md new file mode 100644 index 000000000..c0a332a95 --- /dev/null +++ b/.changeset/six-maps-jog.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": minor +--- + +Schema: replace `propertySignature` constructor with `propertySignatureAnnotations` combinator diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0e4562052..abed5fcee 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,33 +1,33 @@ /* eslint-disable no-undef */ module.exports = { - ignorePatterns: ["dist", "build", "dtslint", "*.mjs", "docs", "*.md"], + ignorePatterns: ["dist", "build", "*.mjs", "*.cjs", "docs", "*.md"], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: 2018, - sourceType: "module" + sourceType: "module", }, settings: { "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"] + "@typescript-eslint/parser": [".ts", ".tsx"], }, "import/resolver": { typescript: { - alwaysTryTypes: true - } - } + alwaysTryTypes: true, + }, + }, }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", - "plugin:@effect/recommended" + "plugin:@effect/recommended", ], plugins: [ "deprecation", "import", "sort-destructure-keys", "simple-import-sort", - "codegen" + "codegen", ], rules: { "codegen/codegen": "error", @@ -50,7 +50,7 @@ module.exports = { "deprecation/deprecation": "off", "@typescript-eslint/array-type": [ "warn", - { default: "generic", readonly: "generic" } + { default: "generic", readonly: "generic" }, ], "@typescript-eslint/member-delimiter-style": 0, "@typescript-eslint/no-non-null-assertion": "off", @@ -62,8 +62,8 @@ module.exports = { "error", { argsIgnorePattern: "^_", - varsIgnorePattern: "^_" - } + varsIgnorePattern: "^_", + }, ], "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/camelcase": "off", @@ -83,9 +83,9 @@ module.exports = { quoteStyle: "alwaysDouble", trailingCommas: "never", operatorPosition: "maintain", - "arrowFunction.useParentheses": "force" - } - } - ] - } -} + "arrowFunction.useParentheses": "force", + }, + }, + ], + }, +}; diff --git a/.vscode/settings.json b/.vscode/settings.json index f33b50997..6d088b2f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,17 @@ { "typescript.tsdk": "node_modules/typescript/lib", - "typescript.preferences.importModuleSpecifier": "project-relative", + "typescript.preferences.importModuleSpecifier": "relative", "typescript.enablePromptUseWorkspaceTsdk": true, "editor.formatOnSave": true, "eslint.format.enable": true, "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.json-language-features" }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" diff --git a/README.md b/README.md index eff90ae66..e77e5a4ae 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,9 @@ const schema: Schema.Schema<{ }> */ const schema = Schema.struct({ - myfield: Schema.optional(Schema.string.pipe(Schema.nonEmpty())), + myfield: Schema.optional(Schema.string.pipe(Schema.nonEmpty()), { + exact: true, + }), }); Schema.decodeSync(schema)({ myfield: undefined }); // Error: Type 'undefined' is not assignable to type 'string'.ts(2379) @@ -131,7 +133,9 @@ const schema: Schema.Schema<{ }> */ const schema = Schema.struct({ - myfield: Schema.optional(Schema.string.pipe(Schema.nonEmpty())), + myfield: Schema.optional(Schema.string.pipe(Schema.nonEmpty()), { + exact: true, + }), }); Schema.decodeSync(schema)({ myfield: undefined }); // No type error, but a decoding failure occurs @@ -899,7 +903,7 @@ console.log(PersonEquivalence(john, alice)); // Output: false | `"a"`, `1`, `true` | type literals | `S.literal("a")`, `S.literal(1)`, `S.literal(true)` | | `a${string}` | template literals | `S.templateLiteral(S.literal("a"), S.string)` | | `{ readonly a: string, readonly b: number }` | structs | `S.struct({ a: S.string, b: S.number })` | -| `{ readonly a?: string }` | optional fields | `S.struct({ a: S.optional(S.string) })` | +| `{ readonly a?: string }` | optional fields | `S.struct({ a: S.optional(S.string, { exact: true }) })` | | `Record` | records | `S.record(A, B)` | | `readonly [string, number]` | tuples | `S.tuple(S.string, S.number)` | | `ReadonlyArray` | arrays | `S.array(S.string)` | @@ -1474,67 +1478,126 @@ S.mutable(S.struct({ a: S.string, b: S.number })); ### Optional fields -```ts -// $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean) }); -``` - -**Note**. The `optional` constructor only exists to be used in combination with the `struct` API to signal an optional field and does not have a broader meaning. This means that it is only allowed to use it as an outer wrapper of a `Schema` and **it cannot be followed by other combinators**, for example this type of operation is prohibited: - -```ts -S.struct({ - // the use of S.optional should be the last step in the pipeline and not preceeded by other combinators like S.nullable - c: S.boolean.pipe(S.optional, S.nullable), // type checker error -}); -``` - -and it must be rewritten like this: - -```ts -S.struct({ - c: S.boolean.pipe(S.nullable, S.optional), // ok -}); -``` - -#### Default values - -Optional fields can be configured to accept a default value, making the field optional in input and required in output: - -```ts -// $ExpectType Schema<{ readonly a?: number; }, { readonly a: number; }> -const schema = S.struct({ a: S.optional(S.number).withDefault(() => 0) }); - -const parse = S.parseSync(schema); - -parse({}); // { a: 0 } -parse({ a: 1 }); // { a: 1 } - -const encode = S.encodeSync(schema); - -encode({ a: 0 }); // { a: 0 } -encode({ a: 1 }); // { a: 1 } -``` - -#### Optional fields as `Option`s - -Optional fields can be configured to transform a value of type `A` into `Option`, making the field optional in input and required in output: - -```ts -import * as O from "effect/Option" - -// $ExpectType Schema<{ readonly a?: number; }, { readonly a: Option; }> -const schema = S.struct({ a: S.optional(S.number).toOption() }); - -const parse = S.parseSync(schema) - -parse({}) // { a: none() } -parse({ a: 1 }) // { a: some(1) } - -const encode = S.encodeSync(schema) - -encode({ a: O.none() }) // {} -encode({ a: O.some(1) }) // { a: 1 } -``` +**Cheatsheet** + +| Combinator | From | To | +| ---------- | --------------------------------- | --------------------------------------------------------------- | +| `optional` | `Schema` | `PropertySignature` | +| `optional` | `Schema`, `{ exact: true }` | `PropertySignature` | + +#### optional(schema) + +- decoding + - `` -> `` + - `undefined` -> `undefined` + - `i` -> `a` +- encoding + - `` -> `` + - `undefined` -> `undefined` + - `a` -> `i` + +#### optional(schema, { exact: true }) + +- decoding + - `` -> `` + - `i` -> `a` +- encoding + - `` -> `` + - `a` -> `i` + +### Default values + +| Combinator | From | To | +| ---------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | +| `optional` | `Schema`, `{ default: () => A }` | `PropertySignature` | +| `optional` | `Schema`, `{ exact: true, default: () => A }` | `PropertySignature` | +| `optional` | `Schema`, `{ nullable: true, default: () => A }` | `PropertySignature` | +| `optional` | `Schema`, `{ exact: true, nullable: true, default: () => A }` | `PropertySignature` | + +#### optional(schema, { default: () => A }) + +- decoding + - `` -> `` + - `undefined` -> `` + - `i` -> `a` +- encoding + - `a` -> `i` + +#### optional(schema, default, { exact: true, default: () => A }) + +- decoding + - `` -> `` + - `i` -> `a` +- encoding + - `a` -> `i` + +#### optional(schema, default, { nullable: true, default: () => A }) + +- decoding + - `` -> `` + - `undefined` -> `` + - `null` -> `` + - `i` -> `a` +- encoding + - `a` -> `i` + +#### optional(schema, default, { exact: true, nullable: true, default: () => A }) + +- decoding + - `` -> `` + - `null` -> `` + - `i` -> `a` +- encoding + - `a` -> `i` + +### Optional fields as `Option`s + +| Combinator | From | To | +| ---------- | --------------------------------------------------------------- | ------------------------------------------------------------------- | +| `optional` | `Schema`, `{ as: "Option" }` | `PropertySignature, false>` | +| `optional` | `Schema`, `{ exact: true, as: "Option" }` | `PropertySignature, false>` | +| `optional` | `Schema`, `{ nullable: true, as: "Option" }` | `PropertySignature, false>` | +| `optional` | `Schema`, `{ exact: true, nullable: true, as: "Option" }` | `PropertySignature, false>` | + +#### optional(schema, { as: "Option" }) + +- decoding + - `` -> `Option.none()` + - `undefined` -> `Option.none()` + - `i` -> `Option.some(a)` +- encoding + - `Option.none()` -> `` + - `Option.some(a)` -> `i` + +#### optional(schema, { exact: true, as: "Option" }) + +- decoding + - `` -> `Option.none()` + - `i` -> `Option.some(a)` +- encoding + - `Option.none()` -> `` + - `Option.some(a)` -> `i` + +#### optional(schema, { nullable: true, as: "Option" }) + +- decoding + - `` -> `Option.none()` + - `undefined` -> `Option.none()` + - `null` -> `Option.none()` + - `i` -> `Option.some(a)` +- encoding + - `Option.none()` -> `` + - `Option.some(a)` -> `i` + +#### optional(schema, { exact: true, nullable: true, as: "Option" }) + +- decoding + - `` -> `Option.none()` + - `null` -> `Option.none()` + - `i` -> `Option.some(a)` +- encoding + - `Option.none()` -> `` + - `Option.some(a)` -> `i` ### Renaming Properties @@ -1731,7 +1794,7 @@ PersonWithTransform { export class PersonWithTransformFrom extends Person.transformFrom()( { - age: S.optional(S.number).toOption(), + age: S.optional(S.number, { exact: true }).toOption(), }, (input) => Effect.mapBoth(getAge(input.id), { @@ -1769,7 +1832,7 @@ The decision of which API to use, either `transform` or `transformFrom`, depends 2. Using `transformFrom`: - The new transformation starts as soon as the initial input is handled. - You should provide a value `{ age?: number }`. - - Based on this fresh input, the subsequent transformation `{ age: S.optional(S.number).toOption() }` is executed. + - Based on this fresh input, the subsequent transformation `{ age: S.optionalToOption(S.number, { exact: true }) }` is executed. - This approach allows for immediate handling of the input, potentially influencing the subsequent transformations. ## Pick @@ -1815,7 +1878,12 @@ The `required` operation ensures that all properties in a schema are mandatory. ```ts // $ExpectType Schema<{ readonly a: string; readonly b: number; }> -S.required(S.struct({ a: S.optional(S.string), b: S.optional(S.number) })); +S.required( + S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.optional(S.number, { exact: true }), + }) +); ``` ## Records @@ -2556,103 +2624,64 @@ console.log(Equal.equals(person1, person2)); // true ## Option -### Parsing from nullable fields - -The `optionFromNullable` combinator in `@effect/schema/Schema` allows you to specify that a field in a schema is of type `Option` and can be parsed from a required nullable field `A | null`. This is particularly useful when working with JSON data that may contain `null` values for optional fields. +**Cheatsheet** -When parsing a nullable field, the `option` combinator follows these conversion rules: +| Combinator | From | To | +| -------------------- | ----------------------------------- | ------------------------------------------- | +| `option` | `Schema` | `Schema, Option>` | +| `optionFromSelf` | `Schema` | `Schema, Option>` | +| `optionFromNullable` | `Schema` | `Schema>` | +| `optionFromNullish` | `Schema`, `null \| undefined` | `Schema>` | -- `null` parses to `None` -- `A` parses to `Some` - -Here's an example that demonstrates how to use the `optionFromNullable` combinator: +where ```ts -import * as Schema from "@effect/schema/Schema"; -import { Option } from "effect"; - -/* -const schema: Schema<{ - readonly a: string; - readonly b: number | null; -}, { - readonly a: string; - readonly b: Option; -}> -*/ -const schema = Schema.struct({ - a: Schema.string, - b: Schema.optionFromNullable(Schema.number), -}); - -// parsing -const parseOrThrow = Schema.parseSync(schema); - -console.log(parseOrThrow({ a: "hello", b: null })); -/* -Output: -{ - a: "hello", - b: { - _id: "Option", - _tag: "None" - } -} -*/ - -console.log(parseOrThrow({ a: "hello", b: 1 })); -/* -Output: -{ - a: "hello", - b: { - _id: "Option", - _tag: "Some", - value: 1 - } -} -*/ - -parseOrThrow({ a: "hello", b: undefined }); -/* -throws: -Error: error(s) found -└─ ["b"] - ├─ union member - │ └─ Expected null, actual undefined - └─ union member - └─ Expected number, actual undefined -*/ - -parseOrThrow({ a: "hello" }); -/* -throws: -Error: error(s) found -└─ ["b"] - └─ is missing -*/ - -// encoding -const encodeOrThrow = Schema.encodeSync(schema); - -console.log(encodeOrThrow({ a: "hello", b: Option.none() })); -/* -Output: -{ - a: "hello", - b: null -} -*/ - -console.log(encodeOrThrow({ a: "hello", b: Option.some(1) })); -/* -Output: -{ - a: "hello", - b: 1 -} -*/ -``` +type OptionFrom = + | { + readonly _tag: "None"; + } + | { + readonly _tag: "Some"; + readonly value: I; + }; +``` + +### option(schema) + +- decoding + - `{ _tag: "None" }` -> `Option.none()` + - `{ _tag: "Some", value: i }` -> `Option.some(a)` +- encoding + - `Option.none()` -> `{ _tag: "None" }` + - `Option.some(a)` -> `{ _tag: "Some", value: i }` + +### optionFromSelf(schema) + +- decoding + - `Option.none()` -> `Option.none()` + - `Option.some(i)` -> `Option.some(a)` +- encoding + - `Option.none()` -> `Option.none()` + - `Option.some(a)` -> `Option.some(i)` + +### optionFromNullable(schema) + +- decoding + - `null` -> `Option.none()` + - `i` -> `Option.some(a)` +- encoding + - `Option.none()` -> `null` + - `Option.some(a)` -> `i` + +### optionFromNullish(schema, onNoneEncoding) + +- decoding + - `null` -> `Option.none()` + - `undefined` -> `Option.none()` + - `i` -> `Option.some(a)` +- encoding + - `Option.none()` -> `` + - `Option.some(a)` -> `i` ## ReadonlySet diff --git a/docs/modules/Schema.ts.md b/docs/modules/Schema.ts.md index 25ccb17cc..b8b904f9b 100644 --- a/docs/modules/Schema.ts.md +++ b/docs/modules/Schema.ts.md @@ -82,6 +82,7 @@ Added in v1.0.0 - [OptionFrom (type alias)](#optionfrom-type-alias) - [option](#option) - [optionFromNullable](#optionfromnullable) + - [optionFromNullish](#optionfromnullish) - [optionFromSelf](#optionfromself) - [ReadonlyArray filters](#readonlyarray-filters) - [itemsCount](#itemscount) @@ -163,8 +164,10 @@ Added in v1.0.0 - [mutable](#mutable) - [nonEmptyArray](#nonemptyarray) - [nullable](#nullable) + - [nullish](#nullish) - [omit](#omit) - [optionalElement](#optionalelement) + - [orUndefined](#orundefined) - [partial](#partial) - [pick](#pick) - [record](#record) @@ -239,6 +242,8 @@ Added in v1.0.0 - [number transformations](#number-transformations) - [clamp](#clamp) - [numberFromString](#numberfromstring-1) +- [optional](#optional) + - [optionalToRequired](#optionaltorequired) - [parsing](#parsing) - [parse](#parse) - [parseEither](#parseeither) @@ -367,7 +372,6 @@ Added in v1.0.0 - [FromStruct (type alias)](#fromstruct-type-alias) - [Join (type alias)](#join-type-alias) - [JsonOptions (type alias)](#jsonoptions-type-alias) - - [OptionalPropertySignature (interface)](#optionalpropertysignature-interface) - [PropertySignature (interface)](#propertysignature-interface) - [Schema (namespace)](#schema-namespace) - [Variance (interface)](#variance-interface) @@ -378,8 +382,8 @@ Added in v1.0.0 - [ToOptionalKeys (type alias)](#tooptionalkeys-type-alias) - [ToStruct (type alias)](#tostruct-type-alias) - [from](#from) - - [optional](#optional) - - [propertySignature](#propertysignature) + - [optional](#optional-1) + - [propertySignatureAnnotations](#propertysignatureannotations) - [readonlyMapFromSelf](#readonlymapfromself) - [to](#to) - [validation](#validation) @@ -1146,6 +1150,19 @@ export declare const optionFromNullable: (value: Schema) => Schema( + value: Schema, + onNoneEncoding: null | undefined +) => Schema> +``` + +Added in v1.0.0 + ## optionFromSelf **Signature** @@ -2106,6 +2123,18 @@ export declare const nullable: (self: Schema) => Schema( + self: Schema +) => Schema +``` + +Added in v1.0.0 + ## omit **Signature** @@ -2134,6 +2163,16 @@ export declare const optionalElement: ( Added in v1.0.0 +## orUndefined + +**Signature** + +```ts +export declare const orUndefined: (self: Schema) => Schema +``` + +Added in v1.0.0 + ## partial **Signature** @@ -3012,6 +3051,24 @@ export declare const numberFromString: (self: Schema) Added in v1.0.0 +# optional + +## optionalToRequired + +**Signature** + +```ts +export declare const optionalToRequired: ( + from: Schema, + to: Schema, + decode: (o: Option.Option) => B, + encode: (b: B) => Option.Option, + options?: DocAnnotations +) => PropertySignature +``` + +Added in v1.0.0 + # parsing ## parse @@ -4338,26 +4395,12 @@ export type JsonOptions = { Added in v1.0.0 -## OptionalPropertySignature (interface) - -**Signature** - -```ts -export interface OptionalPropertySignature - extends PropertySignature { - readonly withDefault: (value: () => To) => PropertySignature - readonly toOption: () => PropertySignature, false> -} -``` - -Added in v1.0.0 - ## PropertySignature (interface) **Signature** ```ts -export interface PropertySignature extends Schema.Variance { +export interface PropertySignature extends Schema.Variance, Pipeable { readonly FromIsOptional: FromIsOptional readonly ToIsOptional: ToIsOptional } @@ -4478,23 +4521,59 @@ Added in v1.0.0 **Signature** ```ts -export declare const optional: ( - schema: Schema, - options?: DocAnnotations -) => OptionalPropertySignature +export declare const optional: { + ( + schema: Schema, + options: { readonly exact: true; readonly default: () => A; readonly nullable: true } + ): PropertySignature + ( + schema: Schema, + options: { readonly exact: true; readonly default: () => A } + ): PropertySignature + ( + schema: Schema, + options: { readonly exact: true; readonly nullable: true; readonly as: "Option" } + ): PropertySignature, false> + ( + schema: Schema, + options: { readonly exact: true; readonly as: "Option" } + ): PropertySignature, false> + (schema: Schema, options: { readonly exact: true }): PropertySignature + ( + schema: Schema, + options: { readonly default: () => A; readonly nullable: true } + ): PropertySignature + (schema: Schema, options: { readonly default: () => A }): PropertySignature + ( + schema: Schema, + options: { readonly nullable: true; readonly as: "Option" } + ): PropertySignature, false> + ( + schema: Schema, + options: { readonly as: "Option" } + ): PropertySignature, false> + (schema: Schema): PropertySignature +} ``` Added in v1.0.0 -## propertySignature +## propertySignatureAnnotations **Signature** ```ts -export declare const propertySignature: ( - schema: Schema, - options: DocAnnotations -) => PropertySignature +export declare const propertySignatureAnnotations: ( + annotations: DocAnnotations +) => < + S extends + | Schema + | Schema + | PropertySignature + | PropertySignature +>( + self: S +) => S extends Schema ? PropertySignature : S ``` Added in v1.0.0 diff --git a/dtslint/Schema.ts b/dtslint/Schema.ts index f9d4addfc..59af16a2b 100644 --- a/dtslint/Schema.ts +++ b/dtslint/Schema.ts @@ -243,49 +243,102 @@ export type MyModelTo = S.Schema.To S.struct({ a: S.never }) // --------------------------------------------- -// optional +// optional { exact: true } // --------------------------------------------- // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean; }, { readonly a: string; readonly b: number; readonly c?: boolean; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean) }) +S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean, { exact: true }) }) // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: string; }, { readonly a: string; readonly b: number; readonly c?: number; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString) }) - -// piping -// $ExpectType Schema<{ readonly a?: string; }, { readonly a?: string; }> -S.struct({ a: pipe(S.string, S.optional) }) +S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString, { exact: true }) }) // $ExpectType Schema<{ readonly a?: never; }, { readonly a?: never; }> +S.struct({ a: S.optional(S.never, { exact: true }) }) + +// --------------------------------------------- +// optional +// --------------------------------------------- + +// $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean | undefined; }, { readonly a: string; readonly b: number; readonly c?: boolean | undefined; }> +S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean) }) + +// $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: string | undefined; }, { readonly a: string; readonly b: number; readonly c?: number | undefined; }> +S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString) }) + +// $ExpectType Schema<{ readonly a?: undefined; }, { readonly a?: undefined; }> S.struct({ a: S.optional(S.never) }) // --------------------------------------------- -// optional().withDefault() +// optional { exact: true, default: () => A } // --------------------------------------------- // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean; }, { readonly a: string; readonly b: number; readonly c: boolean; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean).withDefault(() => false) }) +S.struct({ + a: S.string, + b: S.number, + c: S.optional(S.boolean, { exact: true, default: () => false }) +}) // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: string; }, { readonly a: string; readonly b: number; readonly c: number; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString).withDefault(() => 0) }) +S.struct({ + a: S.string, + b: S.number, + c: S.optional(S.NumberFromString, { exact: true, default: () => 0 }) +}) -// piping -// $ExpectType Schema<{ readonly a?: string; }, { readonly a: string; }> -S.struct({ a: pipe(S.string, S.optional).withDefault(() => "") }) +// --------------------------------------------- +// optional { default: () => A } +// --------------------------------------------- + +// $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean | undefined; }, { readonly a: string; readonly b: number; readonly c: boolean; }> +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?: string | undefined; }, { readonly a: string; readonly b: number; readonly c: number; }> +S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString, { default: () => 0 }) }) + +// --------------------------------------------- +// optional { nullable: true, default: () => A } +// --------------------------------------------- + +// $ExpectType Schema<{ readonly a?: string | null | undefined; }, { readonly a: number; }> +S.struct({ a: S.optional(S.NumberFromString, { nullable: true, default: () => 0 }) }) + +// $ExpectType Schema<{ readonly a?: string | null; }, { readonly a: number; }> +S.struct({ a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) }) // --------------------------------------------- -// optional().toOption() +// optional { exact: true, as: "Option" } // --------------------------------------------- // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean; }, { readonly a: string; readonly b: number; readonly c: Option; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean).toOption() }) +S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean, { exact: true, as: "Option" }) }) // $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: string; }, { readonly a: string; readonly b: number; readonly c: Option; }> -S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString).toOption() }) +S.struct({ + a: S.string, + b: S.number, + c: S.optional(S.NumberFromString, { exact: true, as: "Option" }) +}) -// piping -// $ExpectType Schema<{ readonly a?: string; }, { readonly a: Option; }> -S.struct({ a: pipe(S.string, S.optional).toOption() }) +// --------------------------------------------- +// optional { as: "Option" } +// --------------------------------------------- + +// $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: boolean | undefined; }, { readonly a: string; readonly b: number; readonly c: Option; }> +S.struct({ a: S.string, b: S.number, c: S.optional(S.boolean, { as: "Option" }) }) + +// $ExpectType Schema<{ readonly a: string; readonly b: number; readonly c?: string | undefined; }, { readonly a: string; readonly b: number; readonly c: Option; }> +S.struct({ a: S.string, b: S.number, c: S.optional(S.NumberFromString, { as: "Option" }) }) + +// --------------------------------------------- +// optional { nullable: true, as: "Option" } +// --------------------------------------------- + +// $ExpectType Schema<{ readonly a?: string | null | undefined; }, { readonly a: Option; }> +S.struct({ a: S.optional(S.NumberFromString, { nullable: true, as: "Option" }) }) + +// $ExpectType Schema<{ readonly a?: string | null; }, { readonly a: Option; }> +S.struct({ a: S.optional(S.NumberFromString, { exact: true, nullable: true, as: "Option" }) }) // --------------------------------------------- // pick @@ -302,14 +355,24 @@ pipe(S.struct({ a: S.string, b: S.NumberFromString, c: S.boolean }), S.pick("a", // --------------------------------------------- // $ExpectType Schema<{ readonly a?: string; readonly b: number; }, { readonly a?: string; readonly b: number; }> -pipe(S.struct({ a: S.optional(S.string), b: S.number, c: S.boolean }), S.pick("a", "b")) +pipe( + S.struct({ a: S.optional(S.string, { exact: true }), b: S.number, c: S.boolean }), + S.pick("a", "b") +) // $ExpectType Schema<{ readonly a?: string; readonly b: string; }, { readonly a?: string; readonly b: number; }> -pipe(S.struct({ a: S.optional(S.string), b: S.NumberFromString, c: S.boolean }), S.pick("a", "b")) +pipe( + S.struct({ a: S.optional(S.string, { exact: true }), b: S.NumberFromString, c: S.boolean }), + S.pick("a", "b") +) // $ExpectType Schema<{ readonly a?: string; readonly b: string; }, { readonly a: string; readonly b: number; }> pipe( - S.struct({ a: S.optional(S.string).withDefault(() => ""), b: S.NumberFromString, c: S.boolean }), + S.struct({ + a: S.optional(S.string, { exact: true, default: () => "" }), + b: S.NumberFromString, + c: S.boolean + }), S.pick("a", "b") ) @@ -328,14 +391,21 @@ pipe(S.struct({ a: S.string, b: S.NumberFromString, c: S.boolean }), S.omit("c") // --------------------------------------------- // $ExpectType Schema<{ readonly a?: string; readonly b: number; }, { readonly a?: string; readonly b: number; }> -pipe(S.struct({ a: S.optional(S.string), b: S.number, c: S.boolean }), S.omit("c")) +pipe(S.struct({ a: S.optional(S.string, { exact: true }), b: S.number, c: S.boolean }), S.omit("c")) // $ExpectType Schema<{ readonly a?: string; readonly b: string; }, { readonly a?: string; readonly b: number; }> -pipe(S.struct({ a: S.optional(S.string), b: S.NumberFromString, c: S.boolean }), S.omit("c")) +pipe( + S.struct({ a: S.optional(S.string, { exact: true }), b: S.NumberFromString, c: S.boolean }), + S.omit("c") +) // $ExpectType Schema<{ readonly a?: string; readonly b: string; }, { readonly a: string; readonly b: number; }> pipe( - S.struct({ a: S.optional(S.string).withDefault(() => ""), b: S.NumberFromString, c: S.boolean }), + S.struct({ + a: S.optional(S.string, { exact: true, default: () => "" }), + b: S.NumberFromString, + c: S.boolean + }), S.omit("c") ) @@ -364,11 +434,17 @@ S.partial(S.struct({ a: S.string, b: S.NumberFromString })) // --------------------------------------------- // $ExpectType Schema<{ readonly a: string; readonly b: number; }, { readonly a: string; readonly b: number; }> -S.required(S.struct({ a: S.optional(S.string), b: S.optional(S.number) })) +S.required( + S.struct({ a: S.optional(S.string, { exact: true }), b: S.optional(S.number, { exact: true }) }) +) // $ExpectType Schema<{ readonly b: string; readonly a: string; readonly c: string; }, { readonly b: number; readonly a: string; readonly c: number; }> S.required( - S.struct({ a: S.optional(S.string), b: S.NumberFromString, c: S.optional(S.NumberFromString) }) + S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.NumberFromString, + c: S.optional(S.NumberFromString, { exact: true }) + }) ) // --------------------------------------------- @@ -413,13 +489,13 @@ pipe( // $ExpectType Schema<{ readonly a: string; readonly b: string; readonly c: string; }, { readonly a: string; readonly b: string; readonly c: string; }> S.extend(S.struct({ a: S.string, b: S.string }), S.struct({ c: S.string })) -// TODO: wait for ts `readonly` fix +// rises an error in TypeScript@5.0 // // $ExpectType Schema<{ readonly [x: string]: string; readonly a: string; readonly b: string; readonly c: string; }, { readonly [x: string]: string; readonly a: string; readonly b: string; readonly c: string; }> // pipe( // S.struct({ a: S.string, b: S.string }), // S.extend(S.struct({ c: S.string })), // S.extend(S.record(S.string, S.string)) -// ); +// ) // --------------------------------------------- // suspend @@ -430,9 +506,9 @@ interface SuspendTo1 { readonly as: ReadonlyArray } const suspend1: S.Schema = S.struct({ - a: S.number, - as: S.array(S.suspend(() => suspend1)) - }) + a: S.number, + as: S.array(S.suspend(() => suspend1)) +}) interface LazyFrom2 { readonly a: string @@ -442,12 +518,10 @@ interface LazyTo2 { readonly a: number readonly as: ReadonlyArray } -const lazy2: S.Schema = - S.struct({ - a: S.NumberFromString, - as: S.array(S.suspend(() => lazy2)) - }) - +const lazy2: S.Schema = S.struct({ + a: S.NumberFromString, + as: S.array(S.suspend(() => lazy2)) +}) // --------------------------------------------- // rename @@ -457,33 +531,33 @@ const lazy2: S.Schema = S.rename(S.struct({ a: S.string, b: S.number }), {}) // $ExpectType Schema<{ readonly a: string; readonly b: number; }, { readonly c: string; readonly b: number; }> -S.rename(S.struct({ a: S.string, b: S.number }), { a: 'c' }) +S.rename(S.struct({ a: S.string, b: S.number }), { a: "c" }) // $ExpectType Schema<{ readonly a: string; readonly b: number; }, { readonly c: string; readonly d: number; }> -S.rename(S.struct({ a: S.string, b: S.number }), { a: 'c', b: 'd' }) +S.rename(S.struct({ a: S.string, b: S.number }), { a: "c", b: "d" }) -const a = Symbol.for('@effect/schema/dtslint/a') +const a = Symbol.for("@effect/schema/dtslint/a") // $ExpectType Schema<{ readonly a: string; readonly b: number; }, { readonly [a]: string; readonly b: number; }> -S.rename(S.struct({ a: S.string, b: S.number }), { a: a }) +S.rename(S.struct({ a: S.string, b: S.number }), { a }) // @ts-expect-error -S.rename(S.struct({ a: S.string, b: S.number }), { c: 'd' }) +S.rename(S.struct({ a: S.string, b: S.number }), { c: "d" }) // @ts-expect-error -S.rename(S.struct({ a: S.string, b: S.number }), { a: 'c', d: 'e' }) +S.rename(S.struct({ a: S.string, b: S.number }), { a: "c", d: "e" }) // $ExpectType Schema<{ readonly a: string; readonly b: number; }, { readonly a: string; readonly b: number; }> S.struct({ a: S.string, b: S.number }).pipe(S.rename({})) // $ExpectType Schema<{ readonly a: string; readonly b: number; }, { readonly c: string; readonly b: number; }> -S.struct({ a: S.string, b: S.number }).pipe(S.rename({ a: 'c' })) +S.struct({ a: S.string, b: S.number }).pipe(S.rename({ a: "c" })) // @ts-expect-error -S.struct({ a: S.string, b: S.number }).pipe(S.rename({ c: 'd' })) +S.struct({ a: S.string, b: S.number }).pipe(S.rename({ c: "d" })) // @ts-expect-error -S.struct({ a: S.string, b: S.number }).pipe(S.rename({ a: 'c', d: 'e' })) +S.struct({ a: S.string, b: S.number }).pipe(S.rename({ a: "c", d: "e" })) // --------------------------------------------- // optionFromSelf @@ -550,7 +624,10 @@ const FromFilter = S.union(S.string, S.number) // $ExpectType Schema pipe(FromFilter, S.filter(predicateFilter1)) -const FromRefinement = S.struct({ a: S.optional(S.string), b: S.optional(S.number) }) +const FromRefinement = S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.optional(S.number, { exact: true }) +}) // $ExpectType Schema<{ readonly a?: string; readonly b?: number; }, { readonly a?: string; readonly b?: number; } & { readonly b: number; }> pipe(FromRefinement, S.filter(S.is(S.struct({ b: S.number })))) @@ -729,7 +806,6 @@ export type MyClassTo = S.Schema.To // $ExpectType Schema<{ readonly a: string; }, { readonly a: string; }> MyClass.struct - class MyTaggedClass extends S.TaggedClass()("MyTaggedClass", { a: S.string }) {} @@ -746,7 +822,6 @@ export type MyTaggedClassTo = S.Schema.To // $ExpectType Schema<{ readonly _tag: "MyTaggedClass"; readonly a: string; }, { readonly _tag: "MyTaggedClass"; readonly a: string; }> MyTaggedClass.struct - class VoidTaggedClass extends S.TaggedClass()("VoidTaggedClass", {}) {} // $ExpectType [props?: void | {}, disableValidation?: boolean | undefined] @@ -805,3 +880,13 @@ S.SecretFromSelf // $ExpectType Schema S.secret(S.string) + +// --------------------------------------------- +// propertySignatureAnnotations +// --------------------------------------------- + +// $ExpectType PropertySignature +S.string.pipe(S.propertySignatureAnnotations({ description: "description" })) + +// $ExpectType PropertySignature +S.optional(S.string).pipe(S.propertySignatureAnnotations({ description: "description" })) diff --git a/dtslint/zod.2654.ts b/dtslint/zod.2654.ts index a872c7b44..193e50dfb 100644 --- a/dtslint/zod.2654.ts +++ b/dtslint/zod.2654.ts @@ -3,19 +3,19 @@ import * as S from "@effect/schema/Schema" const schema1 = S.union(S.string, S.array(S.string)) // $ExpectType string | readonly string[] -type Schema1 = S.Schema.To +type _Schema1 = S.Schema.To const schema2 = S.struct({ name: S.union(S.string, S.array(S.string)) }) // $ExpectType { readonly name: string | readonly string[]; } -type Schema2 = S.Schema.To +type _Schema2 = S.Schema.To const schema3 = S.union(S.string, S.record(S.string, S.string)) // $ExpectType string | { readonly [x: string]: string; } -type Schema3 = S.Schema.To +type _Schema3 = S.Schema.To const schema4 = S.struct({ values: S.union(S.string, S.record(S.string, S.string)) }) // $ExpectType { readonly values: string | { readonly [x: string]: string; }; } -type Schema4 = S.Schema.To +type _Schema4 = S.Schema.To diff --git a/src/Arbitrary.ts b/src/Arbitrary.ts index 479a53e92..e533841e2 100644 --- a/src/Arbitrary.ts +++ b/src/Arbitrary.ts @@ -90,14 +90,13 @@ type Options = { /** @internal */ export const go = (ast: AST.AST, options: Options): Arbitrary => { switch (ast._tag) { - case "Declaration": - return pipe( - getHook(ast), - Option.match({ - onNone: () => go(ast.type, options), - onSome: (handler) => handler(...ast.typeParameters.map((p) => go(p, options))) - }) - ) + case "Declaration": { + const hook = getHook(ast) + if (Option.isSome(hook)) { + return hook.value(...ast.typeParameters.map((p) => go(p, options))) + } + throw new Error("cannot build an Arbitrary for a declaration without annotations") + } case "Literal": return (fc) => fc.constant(ast.literal) case "UniqueSymbol": diff --git a/src/Schema.ts b/src/Schema.ts index 75f931bce..bc7177640 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -16,7 +16,6 @@ import * as Equal from "effect/Equal" import * as Equivalence from "effect/Equivalence" import * as Exit from "effect/Exit" import * as FiberId from "effect/FiberId" -import type { LazyArg } from "effect/Function" import { dual, identity } from "effect/Function" import * as N from "effect/Number" import * as Option from "effect/Option" @@ -244,7 +243,7 @@ export { * @since 1.0.0 */ export const isSchema = (u: unknown): u is Schema => - Predicate.isObject(u) && TypeId in u && "ast" in u && "pipe" in u + Predicate.isObject(u) && TypeId in u && "ast" in u // --------------------------------------------- // constructors @@ -442,11 +441,11 @@ export const instanceOf = any>( ): Schema, InstanceType> => { return declare( [], - struct({}), - () => (input, _, ast) => - input instanceof constructor ? - ParseResult.succeed(input) - : ParseResult.fail(ParseResult.type(ast, input)), + unknown, + () => (u, _, ast) => + u instanceof constructor ? + ParseResult.succeed(u) + : ParseResult.fail(ParseResult.type(ast, u)), { [AST.TypeAnnotationId]: InstanceOfTypeId, [InstanceOfTypeId]: { constructor }, @@ -558,6 +557,22 @@ export const union = >>( export const nullable = (self: Schema): Schema => union(_null, self) +/** + * @category combinators + * @since 1.0.0 + */ +export const orUndefined = ( + self: Schema +): Schema => union(_undefined, self) + +/** + * @category combinators + * @since 1.0.0 + */ +export const nullish = ( + self: Schema +): Schema => union(_null, _undefined, self) + /** * @category combinators * @since 1.0.0 @@ -646,100 +661,227 @@ export const nonEmptyArray = ( * @since 1.0.0 */ export interface PropertySignature - extends Schema.Variance + extends Schema.Variance, Pipeable { readonly FromIsOptional: FromIsOptional readonly ToIsOptional: ToIsOptional } -/** - * @since 1.0.0 - */ -export interface OptionalPropertySignature - extends PropertySignature -{ - readonly withDefault: (value: () => To) => PropertySignature - readonly toOption: () => PropertySignature, false> -} - -type PropertySignatureConfig = +type PropertySignatureAST = | { - readonly _tag: "PropertySignature" - readonly ast: AST.AST - readonly annotations: AST.Annotations + readonly _tag: "Declaration" + readonly from: AST.AST + readonly isOptional: boolean + readonly annotations?: AST.Annotations | undefined } | { - readonly _tag: "Optional" - readonly ast: AST.AST - readonly annotations: AST.Annotations | undefined - } - | { - readonly _tag: "Default" - readonly ast: AST.AST - readonly value: LazyArg - readonly annotations: AST.Annotations | undefined - } - | { - readonly _tag: "Option" - readonly ast: AST.AST - readonly annotations: AST.Annotations | undefined + readonly _tag: "OptionalToRequired" + readonly from: AST.AST + readonly to: AST.AST + readonly decode: AST.FinalPropertySignatureTransformation["decode"] + readonly encode: AST.FinalPropertySignatureTransformation["encode"] + readonly annotations?: AST.Annotations | undefined } /** @internal */ export class PropertySignatureImpl { - readonly [TypeId] = variance + readonly [TypeId]: { + readonly From: (_: From) => From + readonly To: (_: To) => To + } = variance readonly FromIsOptional!: FromIsOptional readonly ToIsOptional!: ToIsOptional constructor( - readonly config: PropertySignatureConfig + readonly propertySignatureAST: PropertySignatureAST ) {} - withDefault(value: () => To): PropertySignature { - return new PropertySignatureImpl( - { - _tag: "Default", - ast: this.config.ast, - value, - annotations: this.config.annotations - } - ) + pipe() { + return pipeArguments(this, arguments) } +} - toOption(): PropertySignature, false> { +/** + * @since 1.0.0 + */ +export const propertySignatureAnnotations = + (annotations: DocAnnotations) => + ( + self: S + ): S extends Schema ? PropertySignature : S => { + if (isSchema(self)) { + return new PropertySignatureImpl({ + _tag: "Declaration", + from: self.ast, + isOptional: false, + annotations: toAnnotations(annotations) + }) as any + } return new PropertySignatureImpl({ - _tag: "Option", - ast: this.config.ast, - annotations: this.config.annotations - }) + ...(self as any).propertySignatureAST, + annotations: toAnnotations(annotations) + }) as any } -} /** + * @category optional * @since 1.0.0 */ -export const propertySignature = ( - schema: Schema, - options: DocAnnotations -): PropertySignature => +export const optionalToRequired = ( + from: Schema, + to: Schema, + decode: (o: Option.Option) => B, // `none` here means: the value is missing in the input + encode: (b: B) => Option.Option, // `none` here means: the value will be missing in the output + options?: DocAnnotations +): PropertySignature => new PropertySignatureImpl({ - _tag: "PropertySignature", - ast: schema.ast, + _tag: "OptionalToRequired", + from: from.ast, + to: to.ast, + decode: (o) => Option.some(decode(o)), + encode: Option.flatMap(encode), annotations: toAnnotations(options) }) /** * @since 1.0.0 */ -export const optional = ( +export const optional: { + ( + schema: Schema, + options: { readonly exact: true; readonly default: () => A; readonly nullable: true } + ): PropertySignature + ( + schema: Schema, + options: { readonly exact: true; readonly default: () => A } + ): PropertySignature + ( + schema: Schema, + options: { readonly exact: true; readonly nullable: true; readonly as: "Option" } + ): PropertySignature, false> + ( + schema: Schema, + options: { readonly exact: true; readonly as: "Option" } + ): PropertySignature, false> + ( + schema: Schema, + options: { readonly exact: true } + ): PropertySignature + ( + schema: Schema, + options: { readonly default: () => A; readonly nullable: true } + ): PropertySignature + ( + schema: Schema, + options: { readonly default: () => A } + ): PropertySignature + ( + schema: Schema, + options: { readonly nullable: true; readonly as: "Option" } + ): PropertySignature, false> + ( + schema: Schema, + options: { readonly as: "Option" } + ): PropertySignature, false> + (schema: Schema): PropertySignature +} = ( schema: Schema, - options?: DocAnnotations -): OptionalPropertySignature => - new PropertySignatureImpl({ - _tag: "Optional", - ast: schema.ast, - annotations: toAnnotations(options) - }) + options?: { + readonly exact?: true + readonly default?: () => A + readonly nullable?: true + readonly as?: "Option" + } +): PropertySignature => { + const isExact = options?.exact + const value = options?.default + const isNullable = options?.nullable + const asOption = options?.as == "Option" + + if (isExact) { + if (value) { + if (isNullable) { + return optionalToRequired( + nullable(schema), + to(schema), + Option.match({ onNone: value, onSome: (a) => a === null ? value() : a }), + Option.some + ) + } else { + return optionalToRequired( + schema, + to(schema), + Option.match({ onNone: value, onSome: identity }), + Option.some + ) + } + } else { + if (asOption) { + if (isNullable) { + return optionalToRequired( + nullable(schema), + optionFromSelf(to(schema)), + Option.filter(Predicate.isNotNull), + identity + ) + } else { + return optionalToRequired( + schema, + optionFromSelf(to(schema)), + identity, + identity + ) + } + } + return new PropertySignatureImpl({ + _tag: "Declaration", + from: schema.ast, + isOptional: true + }) + } + } else { + if (value) { + if (isNullable) { + return optionalToRequired( + nullish(schema), + to(schema), + Option.match({ onNone: value, onSome: (a) => (a == null ? value() : a) }), + Option.some + ) + } else { + return optionalToRequired( + orUndefined(schema), + to(schema), + Option.match({ onNone: value, onSome: (a) => (a === undefined ? value() : a) }), + Option.some + ) + } + } else { + if (asOption) { + if (isNullable) { + return optionalToRequired( + nullish(schema), + optionFromSelf(to(schema)), + Option.filter((a: A | null | undefined): a is A => a != null), + identity + ) + } else { + return optionalToRequired( + orUndefined(schema), + optionFromSelf(to(schema)), + Option.filter(Predicate.isNotUndefined), + identity + ) + } + } + return new PropertySignatureImpl({ + _tag: "Declaration", + from: orUndefined(schema).ast, + isOptional: true + }) + } + } +} /** * @since 1.0.0 @@ -790,79 +932,54 @@ export type ToStruct = * @category combinators * @since 1.0.0 */ -export const struct = < - Fields extends StructFields ->( +export const struct = ( fields: Fields -): Schema< - Simplify>, - Simplify> -> => { +): Schema>, Simplify>> => { const ownKeys = Internal.ownKeys(fields) const pss: Array = [] - const froms: Array = [] - const tos: Array = [] - const propertySignatureTransformations: Array = [] + const pssFrom: Array = [] + const pssTo: Array = [] + const psTransformations: Array = [] for (let i = 0; i < ownKeys.length; i++) { const key = ownKeys[i] const field = fields[key] as any - if ("config" in field) { - const config: PropertySignatureConfig = field.config - const from = config.ast - const to = AST.to(from) - const annotations = config.annotations - switch (config._tag) { - case "PropertySignature": - pss.push(AST.createPropertySignature(key, from, false, true, annotations)) - froms.push(AST.createPropertySignature(key, from, false, true)) - tos.push(AST.createPropertySignature(key, to, false, true, annotations)) - break - case "Optional": - pss.push(AST.createPropertySignature(key, from, true, true, annotations)) - froms.push(AST.createPropertySignature(key, from, true, true)) - tos.push(AST.createPropertySignature(key, to, true, true, annotations)) - break - case "Default": - froms.push(AST.createPropertySignature(key, from, true, true)) - tos.push(AST.createPropertySignature(key, to, false, true, annotations)) - propertySignatureTransformations.push( - AST.createPropertySignatureTransform( - key, - key, - AST.createFinalPropertySignatureTransformation( - Option.orElse(() => Option.some(config.value())), - identity - ) - ) + if ("propertySignatureAST" in field) { + const psAst: PropertySignatureAST = field.propertySignatureAST + const from = psAst.from + const annotations = psAst.annotations + switch (psAst._tag) { + case "Declaration": + pss.push(AST.createPropertySignature(key, from, psAst.isOptional, true, annotations)) + pssFrom.push(AST.createPropertySignature(key, from, psAst.isOptional, true)) + pssTo.push( + AST.createPropertySignature(key, AST.to(from), psAst.isOptional, true, annotations) ) break - case "Option": - froms.push(AST.createPropertySignature(key, from, true, true)) - tos.push( - AST.createPropertySignature(key, optionFromSelf(make(to)).ast, false, true, annotations) - ) - propertySignatureTransformations.push( + case "OptionalToRequired": + pssFrom.push(AST.createPropertySignature(key, from, true, true)) + pssTo.push(AST.createPropertySignature(key, psAst.to, false, true, annotations)) + psTransformations.push( AST.createPropertySignatureTransform( key, key, - AST.createFinalPropertySignatureTransformation(Option.some, Option.flatten) + AST.createFinalPropertySignatureTransformation(psAst.decode, psAst.encode) ) ) break } } else { pss.push(AST.createPropertySignature(key, field.ast, false, true)) - froms.push(AST.createPropertySignature(key, field.ast, false, true)) - tos.push(AST.createPropertySignature(key, AST.to(field.ast), false, true)) + pssFrom.push(AST.createPropertySignature(key, field.ast, false, true)) + pssTo.push(AST.createPropertySignature(key, AST.to(field.ast), false, true)) } } - if (ReadonlyArray.isNonEmptyReadonlyArray(propertySignatureTransformations)) { + if (ReadonlyArray.isNonEmptyReadonlyArray(psTransformations)) { return make( AST.createTransform( - AST.createTypeLiteral(froms, []), - AST.createTypeLiteral(tos, []), + AST.createTypeLiteral(pssFrom, []), + AST.createTypeLiteral(pssTo, []), AST.createTypeLiteralTransformation( - propertySignatureTransformations + psTransformations ) ) ) @@ -2995,7 +3112,7 @@ export const BigintFromNumber: Schema = bigintFromNumber(number) */ export const SecretFromSelf: Schema = declare( [], - struct({}), + string, () => (u, _, ast) => Secret.isSecret(u) ? ParseResult.succeed(u) @@ -3047,7 +3164,21 @@ export { */ export const DurationFromSelf: Schema = declare( [], - struct({}), + struct({ + value: union( + struct({ + _tag: literal("Millis"), + millis: number + }), + struct({ + _tag: literal("Nanos"), + nanos: bigintFromSelf + }), + struct({ + _tag: literal("Infinity") + }) + ) + }), () => (u, _, ast) => Duration.isDuration(u) ? ParseResult.succeed(u) @@ -3087,8 +3218,8 @@ export const durationFromHrTime = Duration.nanos(BigInt(s) * BigInt(1e9) + BigInt(n)), - (n) => Duration.toHrTime(n), + ([seconds, nanos]) => Duration.nanos(BigInt(seconds) * BigInt(1e9) + BigInt(nanos)), + (duration) => Duration.toHrTime(duration), { strict: false } ) @@ -3107,10 +3238,10 @@ export const durationFromNanos = ( DurationFromSelf, (nanos) => ParseResult.succeed(Duration.nanos(nanos)), (duration, _, ast) => - Duration.toNanos(duration).pipe(Option.match({ + Option.match(Duration.toNanos(duration), { onNone: () => ParseResult.fail(ParseResult.type(ast, duration)), onSome: (val) => ParseResult.succeed(val) - })), + }), { strict: false } ) @@ -3340,7 +3471,7 @@ export const betweenDuration = ( */ export const Uint8ArrayFromSelf: Schema = declare( [], - struct({}), + array(number), () => (u, _, ast) => Predicate.isUint8Array(u) ? ParseResult.succeed(u) @@ -3371,7 +3502,7 @@ export const uint8ArrayFromNumbers = >( self, Uint8ArrayFromSelf, (a) => Uint8Array.from(a), - (n) => Array.from(n), + (arr) => Array.from(arr), { strict: false } ) @@ -3624,7 +3755,9 @@ const datePretty = (): Pretty.Pretty => (date) => `new Date(${JSON.stringi */ export const DateFromSelf: Schema = declare( [], - struct({}), + struct({ + valueOf: number + }), () => (u, _, ast) => Predicate.isDate(u) ? ParseResult.succeed(u) @@ -3780,6 +3913,21 @@ export const optionFromNullable = ( ): Schema> => transform(nullable(value), optionFromSelf(to(value)), Option.fromNullable, Option.getOrNull) +/** + * @category Option transformations + * @since 1.0.0 + */ +export const optionFromNullish = ( + value: Schema, + onNoneEncoding: null | undefined +): Schema> => + transform( + nullish(value), + optionFromSelf(to(value)), + Option.fromNullable, + onNoneEncoding === null ? Option.getOrNull : Option.getOrUndefined + ) + // --------------------------------------------- // Either transformations // --------------------------------------------- @@ -3926,7 +4074,8 @@ export const readonlyMapFromSelf = ( return declare( [key, value], struct({ - size: number + size: number, + entries: array(tuple(key, value)) }), (isDecoding, key, value) => { const items = array(tuple(key, value)) @@ -3991,7 +4140,8 @@ export const readonlySetFromSelf = ( return declare( [item], struct({ - size: number + size: number, + values: array(item) }), (isDecoding, item) => { const items = array(item) @@ -4038,7 +4188,10 @@ const bigDecimalArbitrary = (): Arbitrary => (fc) => */ export const BigDecimalFromSelf: Schema = declare( [], - struct({}), + struct({ + value: bigintFromSelf, + scale: number + }), () => (u, _, ast) => BigDecimal.isBigDecimal(u) ? ParseResult.succeed(u) @@ -4381,8 +4534,8 @@ export const chunkFromSelf = (item: Schema): Schema, return declare( [item], struct({ - _id: uniqueSymbol(Symbol.for("effect/Chunk")), - length: number + length: number, + values: array(item) }), (isDecoding, item) => { const items = array(item) diff --git a/test/AST/createUnion.test.ts b/test/AST/createUnion.test.ts index 3c97b4837..e6088c2da 100644 --- a/test/AST/createUnion.test.ts +++ b/test/AST/createUnion.test.ts @@ -65,7 +65,10 @@ describe("AST/createUnion", () => { it("1 required vs 2 optional", () => { const a = S.struct({ a: S.string }) - const ab = S.struct({ a: S.optional(S.string), b: S.optional(S.number) }) + const ab = S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.optional(S.number, { exact: true }) + }) const schema = S.union(a, ab) expect(schema.ast).toEqual({ _tag: "Union", diff --git a/test/Arbitrary/Arbitrary.test.ts b/test/Arbitrary/Arbitrary.test.ts index a4832e3b4..ed3c42d9c 100644 --- a/test/Arbitrary/Arbitrary.test.ts +++ b/test/Arbitrary/Arbitrary.test.ts @@ -1,4 +1,5 @@ import * as Arbitrary from "@effect/schema/Arbitrary" +import * as ParseResult from "@effect/schema/ParseResult" import * as S from "@effect/schema/Schema" import { propertyFrom, propertyTo } from "@effect/schema/test/util" import * as fc from "fast-check" @@ -9,6 +10,13 @@ describe("Arbitrary/Arbitrary", () => { expect(Arbitrary.ArbitraryHookId).exist }) + it("should throw on declarations without annotations", () => { + const schema = S.declare([], S.any, () => ParseResult.succeed) + expect(() => Arbitrary.go(schema.ast, {})).toThrow( + new Error("cannot build an Arbitrary for a declaration without annotations") + ) + }) + it("should throw on transformations", () => { const schema = S.NumberFromString expect(() => Arbitrary.go(schema.ast, {})).toThrow( @@ -327,12 +335,12 @@ describe("Arbitrary/Arbitrary", () => { }) it("optional property signature", () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) propertyTo(schema) }) it("optional property signature with undefined", () => { - const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined)) }) + const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) propertyTo(schema) }) diff --git a/test/Arbitrary/Class.test.ts b/test/Arbitrary/Class.test.ts index 0b9138b98..c035d494f 100644 --- a/test/Arbitrary/Class.test.ts +++ b/test/Arbitrary/Class.test.ts @@ -19,14 +19,14 @@ describe("class", () => { it("optional property signature", () => { class Class extends S.Class()({ - a: S.optional(S.number) + a: S.optional(S.number, { exact: true }) }) {} propertyTo(Class) }) it("optional property signature with undefined", () => { class Class extends S.Class()({ - a: S.optional(S.union(S.number, S.undefined)) + a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) {} propertyTo(Class) }) diff --git a/test/Chunk/chunkFromSelf.test.ts b/test/Chunk/chunkFromSelf.test.ts index 8c1cea70f..316492444 100644 --- a/test/Chunk/chunkFromSelf.test.ts +++ b/test/Chunk/chunkFromSelf.test.ts @@ -6,12 +6,6 @@ import * as C from "effect/Chunk" import { describe, expect, it } from "vitest" describe("Chunk/chunkFromSelf", () => { - it("keyof", () => { - expect(S.keyof(S.chunkFromSelf(S.string))).toEqual( - S.union(S.literal("_id"), S.literal("length")) - ) - }) - it("property tests", () => { Util.roundtrip(S.chunkFromSelf(S.number)) }) diff --git a/test/Date/DateFromSelf.test.ts b/test/Date/DateFromSelf.test.ts index e6135a8b6..3b2dee928 100644 --- a/test/Date/DateFromSelf.test.ts +++ b/test/Date/DateFromSelf.test.ts @@ -4,10 +4,6 @@ import * as Util from "@effect/schema/test/util" import { describe, expect, it } from "vitest" describe("Date/DateFromSelf", () => { - it("keyof", () => { - expect(S.keyof(S.DateFromSelf)).toEqual(S.never) - }) - it("property tests", () => { Util.roundtrip(S.DateFromSelf) }) diff --git a/test/Equivalence.test.ts b/test/Equivalence.test.ts index 87c40b3ed..0af97af5e 100644 --- a/test/Equivalence.test.ts +++ b/test/Equivalence.test.ts @@ -455,8 +455,8 @@ describe("Equivalence", () => { it("optional property signature", () => { const schema = S.struct({ - a: S.optional(string), - b: S.optional(S.union(number, S.undefined)) + a: S.optional(string, { exact: true }), + b: S.optional(S.union(number, S.undefined), { exact: true }) }) const equivalence = E.to(schema) diff --git a/test/JSONSchema.test.ts b/test/JSONSchema.test.ts index df4569cb2..5eedf158b 100644 --- a/test/JSONSchema.test.ts +++ b/test/JSONSchema.test.ts @@ -698,7 +698,10 @@ describe("JSONSchema", () => { }) it("optional property signature", () => { - const schema = Schema.struct({ a: Schema.string, b: Schema.optional(JsonNumber) }) + const schema = Schema.struct({ + a: Schema.string, + b: Schema.optional(JsonNumber, { exact: true }) + }) const jsonSchema = JSONSchema.to(schema) expect(jsonSchema).toStrictEqual({ "$schema": "http://json-schema.org/draft-07/schema#", @@ -1363,16 +1366,16 @@ describe("JSONSchema", () => { it("struct properties support", () => { const schema = Schema.struct({ - foo: Schema.propertySignature(Schema.string, { + foo: Schema.string.pipe(Schema.propertySignatureAnnotations({ description: "foo description", title: "foo title", examples: ["foo example"] - }), - bar: Schema.propertySignature(JsonNumber, { + })), + bar: JsonNumber.pipe(Schema.propertySignatureAnnotations({ description: "bar description", title: "bar title", examples: ["bar example"] - }) + })) }) const jsonSchema = JSONSchema.to(schema) expect(jsonSchema).toEqual({ diff --git a/test/Option/optionFromNullish.test.ts b/test/Option/optionFromNullish.test.ts new file mode 100644 index 000000000..651c7ec6e --- /dev/null +++ b/test/Option/optionFromNullish.test.ts @@ -0,0 +1,40 @@ +import * as S from "@effect/schema/Schema" +import * as Util from "@effect/schema/test/util" +import * as O from "effect/Option" +import { describe, expect, it } from "vitest" + +describe("Option/optionFromNullish", () => { + it("property tests", () => { + Util.roundtrip(S.optionFromNullish(S.number, null)) + Util.roundtrip(S.optionFromNullish(S.number, undefined)) + }) + + it("decoding", async () => { + const schema = S.optionFromNullish(S.NumberFromString, undefined) + await Util.expectParseSuccess(schema, null, O.none()) + await Util.expectParseSuccess(schema, undefined, O.none()) + await Util.expectParseSuccess(schema, "1", O.some(1)) + + expect(O.isOption(S.decodeSync(schema)(null))).toEqual(true) + expect(O.isOption(S.decodeSync(schema)(undefined))).toEqual(true) + expect(O.isOption(S.decodeSync(schema)("1"))).toEqual(true) + + await Util.expectParseFailure( + schema, + {}, + `union member: Expected null, actual {}, union member: Expected undefined, actual {}, union member: Expected string, actual {}` + ) + }) + + it("encoding null", async () => { + const schema = S.optionFromNullish(S.NumberFromString, null) + await Util.expectEncodeSuccess(schema, O.none(), null) + await Util.expectEncodeSuccess(schema, O.some(1), "1") + }) + + it("encoding undefined", async () => { + const schema = S.optionFromNullish(S.NumberFromString, undefined) + await Util.expectEncodeSuccess(schema, O.none(), undefined) + await Util.expectEncodeSuccess(schema, O.some(1), "1") + }) +}) diff --git a/test/Pretty.test.ts b/test/Pretty.test.ts index e74834e13..d7b23237c 100644 --- a/test/Pretty.test.ts +++ b/test/Pretty.test.ts @@ -168,7 +168,7 @@ describe("Pretty", () => { }) it("struct/ should not output optional property signatures", () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) const pretty = P.to(schema) expect(pretty({})).toEqual("{}") expect(pretty({ a: 1 })).toEqual(`{ "a": 1 }`) @@ -198,7 +198,7 @@ describe("Pretty", () => { }) it("struct/ optional property signature", () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) const pretty = P.to(schema) expect(pretty({})).toEqual(`{}`) expect(pretty({ a: 1 })).toEqual(`{ "a": 1 }`) @@ -207,7 +207,7 @@ describe("Pretty", () => { }) it("struct/ optional property signature with undefined", () => { - const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined)) }) + const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) const pretty = P.to(schema) expect(pretty({})).toEqual(`{}`) expect(pretty({ a: 1 })).toEqual(`{ "a": 1 }`) diff --git a/test/ReadonlyMap/readonlyMapFromSelf.test.ts b/test/ReadonlyMap/readonlyMapFromSelf.test.ts index fa6ea8c56..966eb705e 100644 --- a/test/ReadonlyMap/readonlyMapFromSelf.test.ts +++ b/test/ReadonlyMap/readonlyMapFromSelf.test.ts @@ -5,10 +5,6 @@ import * as Util from "@effect/schema/test/util" import { describe, expect, it } from "vitest" describe("ReadonlyMap/readonlyMapFromSelf", () => { - it("keyof", () => { - expect(S.keyof(S.readonlyMapFromSelf(S.number, S.string))).toEqual(S.literal("size")) - }) - it("property tests", () => { Util.roundtrip(S.readonlyMapFromSelf(S.number, S.string)) }) diff --git a/test/ReadonlySet/readonlySetFromSelf.test.ts b/test/ReadonlySet/readonlySetFromSelf.test.ts index e972d9af1..7062e0d6c 100644 --- a/test/ReadonlySet/readonlySetFromSelf.test.ts +++ b/test/ReadonlySet/readonlySetFromSelf.test.ts @@ -5,10 +5,6 @@ import * as Util from "@effect/schema/test/util" import { describe, expect, it } from "vitest" describe("ReadonlySet/readonlySetFromSelf", () => { - it("keyof", () => { - expect(S.keyof(S.readonlySetFromSelf(S.number))).toEqual(S.literal("size")) - }) - it("property tests", () => { Util.roundtrip(S.readonlySetFromSelf(S.number)) }) diff --git a/test/Schema/Class.test.ts b/test/Schema/Class.test.ts index f686432cc..7ff3e94fc 100644 --- a/test/Schema/Class.test.ts +++ b/test/Schema/Class.test.ts @@ -52,7 +52,7 @@ class PersonWithNick extends PersonWithAge.extend()({ class PersonWithTransform extends Person.transform()( { id: S.string, - thing: S.optional(S.struct({ id: S.number })).toOption() + thing: S.optional(S.struct({ id: S.number }), { exact: true, as: "Option" }) }, (input) => PR.succeed({ @@ -70,7 +70,7 @@ class PersonWithTransform extends Person.transform()( class PersonWithTransformFrom extends Person.transformFrom()( { id: S.string, - thing: S.optional(S.struct({ id: S.number })).toOption() + thing: S.optional(S.struct({ id: S.number }), { exact: true, as: "Option" }) }, (input) => PR.succeed({ diff --git a/test/Schema/PropertySignatureTransformations.test.ts b/test/Schema/PropertySignatureTransformations.test.ts index 4e7e8c4d6..d0f034e9f 100644 --- a/test/Schema/PropertySignatureTransformations.test.ts +++ b/test/Schema/PropertySignatureTransformations.test.ts @@ -9,7 +9,7 @@ describe("Schema/PropertySignatureTransformations", () => { it("default", async () => { const transform: S.Schema<{ readonly a?: string }, { readonly a: number }> = S.make( AST.createTransform( - S.struct({ a: S.optional(S.NumberFromString) }).ast, + S.struct({ a: S.optional(S.NumberFromString, { exact: true }) }).ast, S.struct({ a: S.number }).ast, AST.createTypeLiteralTransformation( [ @@ -40,7 +40,7 @@ describe("Schema/PropertySignatureTransformations", () => { it("bidirectional default", async () => { const transform: S.Schema<{ readonly a?: string }, { readonly a: number }> = S.make( AST.createTransform( - S.struct({ a: S.optional(S.NumberFromString) }).ast, + S.struct({ a: S.optional(S.NumberFromString, { exact: true }) }).ast, S.struct({ a: S.number }).ast, AST.createTypeLiteralTransformation( [ @@ -72,7 +72,7 @@ describe("Schema/PropertySignatureTransformations", () => { const transform: S.Schema<{ readonly a?: string }, { readonly a: O.Option }> = S .make( AST.createTransform( - S.struct({ a: S.optional(S.NumberFromString) }).ast, + S.struct({ a: S.optional(S.NumberFromString, { exact: true }) }).ast, S.struct({ a: S.optionFromSelf(S.number) }).ast, AST.createTypeLiteralTransformation( [ @@ -104,7 +104,7 @@ describe("Schema/PropertySignatureTransformations", () => { const transform: S.Schema<{ readonly a: string }, { readonly a?: string }> = S.make( AST.createTransform( S.struct({ a: S.string }).ast, - S.struct({ a: S.optional(S.string) }).ast, + S.struct({ a: S.optional(S.string, { exact: true }) }).ast, AST.createTypeLiteralTransformation( [ AST.createPropertySignatureTransform( @@ -179,7 +179,7 @@ describe("Schema/PropertySignatureTransformations", () => { const transform: S.Schema<{ readonly a: string }, { readonly a?: number }> = S.make( AST.createTransform( S.struct({ a: S.number }).ast, - S.struct({ a: S.optional(S.number) }).ast, + S.struct({ a: S.optional(S.number, { exact: true }) }).ast, AST.createTypeLiteralTransformation( [ AST.createPropertySignatureTransform( diff --git a/test/Schema/attachPropertySignature.test.ts b/test/Schema/attachPropertySignature.test.ts index fe44c3988..ae7cb07ae 100644 --- a/test/Schema/attachPropertySignature.test.ts +++ b/test/Schema/attachPropertySignature.test.ts @@ -76,7 +76,7 @@ describe("Schema/attachPropertySignature", () => { }) it("with a transformation", async () => { - const From = S.struct({ radius: S.number, _isVisible: S.optional(S.boolean) }) + const From = S.struct({ radius: S.number, _isVisible: S.optional(S.boolean, { exact: true }) }) const To = S.struct({ radius: S.number, _isVisible: S.boolean }) const schema = S.transformOrFail( diff --git a/test/Schema/extend.test.ts b/test/Schema/extend.test.ts index f477feb3c..abc62e27c 100644 --- a/test/Schema/extend.test.ts +++ b/test/Schema/extend.test.ts @@ -10,49 +10,60 @@ describe("Schema/extend", () => { }) it(`struct with defaults extend struct`, async () => { - const schema = S.struct({ a: S.optional(S.string).withDefault(() => ""), b: S.string }).pipe( - S.extend(S.struct({ c: S.number })) - ) + const schema = S.struct({ + a: S.optional(S.string, { exact: true, default: () => "" }), + b: S.string + }) + .pipe( + S.extend(S.struct({ c: S.number })) + ) await Util.expectParseSuccess(schema, { b: "b", c: 1 }, { a: "", b: "b", c: 1 }) }) it(`struct extend struct with defaults`, async () => { const schema = S.struct({ a: S.number }).pipe( S.extend( - S.struct({ b: S.string, c: S.optional(S.string).withDefault(() => "") }) + S.struct({ b: S.string, c: S.optional(S.string, { exact: true, default: () => "" }) }) ) ) await Util.expectParseSuccess(schema, { a: 1, b: "b" }, { a: 1, b: "b", c: "" }) }) it(`struct with defaults extend struct with defaults `, async () => { - const schema = S.struct({ a: S.optional(S.string).withDefault(() => ""), b: S.string }).pipe( - S.extend( - S.struct({ c: S.optional(S.number).withDefault(() => 0), d: S.boolean }) + const schema = S.struct({ + a: S.optional(S.string, { exact: true, default: () => "" }), + b: S.string + }) + .pipe( + S.extend( + S.struct({ + c: S.optional(S.number, { exact: true, default: () => 0 }), + d: S.boolean + }) + ) ) - ) await Util.expectParseSuccess(schema, { b: "b", d: true }, { a: "", b: "b", c: 0, d: true }) }) it(`union with defaults extend union with defaults `, async () => { const schema = S.union( S.struct({ - a: S.optional(S.string).withDefault(() => "a"), + a: S.optional(S.string, { exact: true, default: () => "a" }), b: S.string }), S.struct({ - c: S.optional(S.string).withDefault(() => "c"), + c: S.optional(S.string, { exact: true, default: () => "c" }), d: S.string }) ).pipe( S.extend( S.union( S.struct({ - e: S.optional(S.string).withDefault(() => "e"), + e: S.optional(S.string, { exact: true, default: () => "e" }), f: S.string }), S.struct({ - g: S.optional(S.string).withDefault(() => "g"), + g: S.optional(S.string, { exact: true, default: () => "g" }), h: S.string }) ) @@ -227,7 +238,7 @@ describe("Schema/extend", () => { it("optional, transformation", async () => { const schema = S.struct({ - a: S.optional(S.boolean).withDefault(() => true) + a: S.optional(S.boolean, { exact: true, default: () => true }) }).pipe( S.extend( S.struct({ @@ -247,7 +258,7 @@ describe("Schema/extend", () => { }).pipe( S.extend( S.struct({ - a: S.optional(S.boolean).withDefault(() => true) + a: S.optional(S.boolean, { exact: true, default: () => true }) }) ) ) diff --git a/test/Schema/is.test.ts b/test/Schema/is.test.ts index d7f79dc5f..c31924c3f 100644 --- a/test/Schema/is.test.ts +++ b/test/Schema/is.test.ts @@ -289,7 +289,7 @@ describe("Schema/is", () => { }) it("optional property signature", () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) const is = P.is(schema) expect(is({})).toEqual(true) expect(is({ a: 1 })).toEqual(true) @@ -301,7 +301,7 @@ describe("Schema/is", () => { }) it("optional property signature with undefined", () => { - const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined)) }) + const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) const is = P.is(schema) expect(is({})).toEqual(true) expect(is({ a: 1 })).toEqual(true) diff --git a/test/Schema/isSchema.test.ts b/test/Schema/isSchema.test.ts index e2cdcc74d..5597378a5 100644 --- a/test/Schema/isSchema.test.ts +++ b/test/Schema/isSchema.test.ts @@ -4,15 +4,15 @@ import { describe, expect, it } from "vitest" describe("isSchema", () => { it("Schema", () => { expect(S.isSchema(S.string)).toBe(true) - expect(S.isSchema(S.propertySignature(S.string, {}))).toBe(false) - expect(S.isSchema(S.optional(S.string))).toBe(false) + expect(S.isSchema(S.numberFromString)).toBe(false) }) it("BrandSchema", () => { expect(S.isSchema(S.string.pipe(S.brand("my-brand")))).toBe(true) }) - it("Codec", () => { - expect(S.isSchema(S.numberFromString)).toBe(false) + it("PropertySignature", () => { + expect(S.isSchema(S.string.pipe(S.propertySignatureAnnotations({})))).toBe(false) + expect(S.isSchema(S.optional(S.string, { exact: true }))).toBe(false) }) }) diff --git a/test/Schema/omit.test.ts b/test/Schema/omit.test.ts index 195738408..7cfe1267b 100644 --- a/test/Schema/omit.test.ts +++ b/test/Schema/omit.test.ts @@ -24,9 +24,14 @@ describe("Schema/omit", () => { }) it("struct with optionals", async () => { - const schema = S.struct({ a: S.optional(S.string), b: S.NumberFromString, c: S.boolean }).pipe( - S.omit("c") - ) + const schema = S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.NumberFromString, + c: S.boolean + }) + .pipe( + S.omit("c") + ) await Util.expectParseSuccess(schema, { a: "a", b: "1" }, { a: "a", b: 1 }) await Util.expectParseSuccess(schema, { b: "1" }, { b: 1 }) @@ -59,7 +64,7 @@ describe("Schema/omit", () => { it("struct with property signature transformations", async () => { const schema = S.struct({ - a: S.optional(S.string).withDefault(() => ""), + a: S.optional(S.string, { exact: true, default: () => "" }), b: S.NumberFromString, c: S.boolean }).pipe( diff --git a/test/Schema/onExcess.test.ts b/test/Schema/onExcess.test.ts index aeaf54bf2..b24075521 100644 --- a/test/Schema/onExcess.test.ts +++ b/test/Schema/onExcess.test.ts @@ -15,8 +15,11 @@ describe("Schema/onExcess", () => { describe("union should choose the output with more info", () => { it("structs", async () => { - const a = S.struct({ a: S.optional(S.number) }) - const b = S.struct({ a: S.optional(S.number), b: S.optional(S.string) }) + const a = S.struct({ a: S.optional(S.number, { exact: true }) }) + const b = S.struct({ + a: S.optional(S.number, { exact: true }), + b: S.optional(S.string, { exact: true }) + }) const schema = S.union(a, b) await Util.expectParseSuccess( schema, diff --git a/test/Schema/optional.test.ts b/test/Schema/optional.test.ts index 5921544b1..9283bc05a 100644 --- a/test/Schema/optional.test.ts +++ b/test/Schema/optional.test.ts @@ -1,110 +1,204 @@ import * as S from "@effect/schema/Schema" import * as Util from "@effect/schema/test/util" import * as O from "effect/Option" -import { describe, expect, it } from "vitest" +import { describe, it } from "vitest" -describe("optional", () => { - it("should add annotations (optional)", () => { - const schema = S.struct({ - a: S.optional(S.string, { [Symbol.for("custom-annotation")]: "custom-annotation-value" }) +describe("optional APIs", () => { + describe("optional > { exact: true }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { exact: true }) + }) + await Util.expectParseSuccess(schema, {}, {}) + await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) + await Util.expectParseFailure( + schema, + { a: "a" }, + `/a Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, {}, {}) + await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) }) - const ast: any = schema.ast - expect(ast.propertySignatures[0].annotations).toEqual({ - [Symbol.for("custom-annotation")]: "custom-annotation-value" + + it("never", async () => { + const schema = S.struct({ a: S.optional(S.never, { exact: true }), b: S.number }) + await Util.expectParseSuccess(schema, { b: 1 }) + await Util.expectParseFailure(schema, { a: "a", b: 1 }, `/a Expected never, actual "a"`) }) }) - it("should add annotations (optional + withDefault)", () => { - const schema = S.struct({ - a: S.optional(S.string, { [Symbol.for("custom-annotation")]: "custom-annotation-value" }) - .withDefault(() => "") - }) - const ast: any = schema.ast - expect(ast.to.propertySignatures[0].annotations).toEqual({ - [Symbol.for("custom-annotation")]: "custom-annotation-value" + describe("optional", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString) + }) + await Util.expectParseSuccess(schema, {}, {}) + await Util.expectParseSuccess(schema, { a: undefined }, { a: undefined }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) + await Util.expectParseFailure( + schema, + { a: "a" }, + `/a union member: Expected undefined, actual "a", union member: Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, {}, {}) + await Util.expectEncodeSuccess(schema, { a: undefined }, { a: undefined }) + await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) }) }) - it("should add annotations (optional + toOption)", () => { - const schema = S.struct({ - a: S.optional(S.string, { [Symbol.for("custom-annotation")]: "custom-annotation-value" }) - .toOption() - }) - const ast: any = schema.ast - expect(ast.to.propertySignatures[0].annotations).toEqual({ - [Symbol.for("custom-annotation")]: "custom-annotation-value" + describe("optional > { exact: true, as: \"Option\" }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { exact: true, as: "Option" }) + }) + await Util.expectParseSuccess(schema, {}, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectParseFailure(schema, { + a: "a" + }, `/a Expected string <-> number, actual "a"`) + + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, {}) }) }) - it("case Default", async () => { - const transform = S.struct({ - a: S.optional(S.NumberFromString).withDefault(() => 0) + describe("optionalToOption > { exact: true, nullable: true, as: \"Option\" }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { exact: true, nullable: true, as: "Option" }) + }) + await Util.expectParseSuccess(schema, {}, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: null }, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectParseFailure( + schema, + { + a: "a" + }, + `/a union member: Expected null, actual "a", union member: Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, {}) }) - await Util.expectParseSuccess(transform, {}, { a: 0 }) - await Util.expectParseSuccess(transform, { a: "1" }, { a: 1 }) - await Util.expectParseFailure( - transform, - { a: "a" }, - `/a Expected string <-> number, actual "a"` - ) - - await Util.expectEncodeSuccess(transform, { a: 1 }, { a: "1" }) - await Util.expectEncodeSuccess(transform, { a: 0 }, { a: "0" }) }) - it("case Option", async () => { - const transform = S.struct({ a: S.optional(S.NumberFromString).toOption() }) - await Util.expectParseSuccess(transform, {}, { a: O.none() }) - await Util.expectParseSuccess(transform, { a: "1" }, { a: O.some(1) }) - await Util.expectParseFailure(transform, { - a: "a" - }, `/a Expected string <-> number, actual "a"`) + describe("optional > { as: \"Option\" }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ a: S.optional(S.NumberFromString, { as: "Option" }) }) + await Util.expectParseSuccess(schema, {}, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: undefined }, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectParseFailure( + schema, + { + a: "a" + }, + `/a union member: Expected undefined, actual "a", union member: Expected string <-> number, actual "a"` + ) - await Util.expectEncodeSuccess(transform, { a: O.some(1) }, { a: "1" }) - await Util.expectEncodeSuccess(transform, { a: O.none() }, {}) + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, {}) + }) }) - it("never", async () => { - const schema = S.struct({ a: S.optional(S.never), b: S.number }) - await Util.expectParseSuccess(schema, { b: 1 }) - await Util.expectParseFailure(schema, { a: "a", b: 1 }, `/a Expected never, actual "a"`) - }) + describe("optional > { nullable: true, as: \"Option\" }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { nullable: true, as: "Option" }) + }) + await Util.expectParseSuccess(schema, {}, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: undefined }, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: null }, { a: O.none() }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: O.some(1) }) + await Util.expectParseFailure( + schema, + { + a: "a" + }, + `/a union member: Expected null, actual "a", union member: Expected undefined, actual "a", union member: Expected string <-> number, actual "a"` + ) - it("all", async () => { - const transform = S.struct({ - a: S.boolean, - b: S.optional(S.NumberFromString), - c: S.optional(S.Trim).withDefault(() => "-"), - d: S.optional(S.Date).toOption() + await Util.expectEncodeSuccess(schema, { a: O.some(1) }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: O.none() }, {}) }) - await Util.expectParseSuccess(transform, { a: true }, { a: true, c: "-", d: O.none() }) - await Util.expectParseSuccess(transform, { a: true, b: "1" }, { - a: true, - b: 1, - c: "-", - d: O.none() + }) + + describe("optional > { exact: true, default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { exact: true, default: () => 0 }) + }) + await Util.expectParseSuccess(schema, {}, { a: 0 }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) + await Util.expectParseFailure( + schema, + { a: "a" }, + `/a Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) - await Util.expectParseSuccess(transform, { a: true, c: "a" }, { a: true, c: "a", d: O.none() }) - await Util.expectParseSuccess(transform, { a: true, d: "1970-01-01T00:00:00.000Z" }, { - a: true, - c: "-", - d: O.some(new Date(0)) + }) + + describe("optional > { default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { default: () => 0 }) + }) + await Util.expectParseSuccess(schema, {}, { a: 0 }) + await Util.expectParseSuccess(schema, { a: undefined }, { a: 0 }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) + await Util.expectParseFailure( + schema, + { a: "a" }, + `/a union member: Expected undefined, actual "a", union member: Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) - await Util.expectParseSuccess(transform, { a: true, c: "a", d: "1970-01-01T00:00:00.000Z" }, { - a: true, - c: "a", - d: O.some(new Date(0)) + }) + + describe("optional > { nullable: true, default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { nullable: true, default: () => 0 }) + }) + await Util.expectParseSuccess(schema, {}, { a: 0 }) + await Util.expectParseSuccess(schema, { a: null }, { a: 0 }) + await Util.expectParseSuccess(schema, { a: undefined }, { a: 0 }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) + await Util.expectParseFailure( + schema, + { a: "a" }, + `/a union member: Expected null, actual "a", union member: Expected undefined, actual "a", union member: Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) - await Util.expectParseSuccess(transform, { - a: true, - c: "a", - d: "1970-01-01T00:00:00.000Z", - b: "1" - }, { - a: true, - b: 1, - c: "a", - d: O.some(new Date(0)) + }) + + describe("optional > { exact: true, nullable: true, default: () => A }", () => { + it("decoding / encoding", async () => { + const schema = S.struct({ + a: S.optional(S.NumberFromString, { exact: true, nullable: true, default: () => 0 }) + }) + await Util.expectParseSuccess(schema, {}, { a: 0 }) + await Util.expectParseSuccess(schema, { a: null }, { a: 0 }) + await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) + await Util.expectParseFailure( + schema, + { a: "a" }, + `/a union member: Expected null, actual "a", union member: Expected string <-> number, actual "a"` + ) + + await Util.expectEncodeSuccess(schema, { a: 1 }, { a: "1" }) + await Util.expectEncodeSuccess(schema, { a: 0 }, { a: "0" }) }) }) }) diff --git a/test/Schema/pick.test.ts b/test/Schema/pick.test.ts index 7802d0e0e..4a49480f9 100644 --- a/test/Schema/pick.test.ts +++ b/test/Schema/pick.test.ts @@ -24,9 +24,14 @@ describe("Schema/pick", () => { }) it("struct with optionals", async () => { - const schema = S.struct({ a: S.optional(S.string), b: S.NumberFromString, c: S.boolean }).pipe( - S.pick("a", "b") - ) + const schema = S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.NumberFromString, + c: S.boolean + }) + .pipe( + S.pick("a", "b") + ) await Util.expectParseSuccess(schema, { a: "a", b: "1" }, { a: "a", b: 1 }) await Util.expectParseSuccess(schema, { b: "1" }, { b: 1 }) @@ -59,7 +64,7 @@ describe("Schema/pick", () => { it("struct with property signature transformations", async () => { const schema = S.struct({ - a: S.optional(S.string).withDefault(() => ""), + a: S.optional(S.string, { exact: true, default: () => "" }), b: S.NumberFromString, c: S.boolean }).pipe( diff --git a/test/Schema/propertySignature.test.ts b/test/Schema/propertySignature.test.ts deleted file mode 100644 index de59a77f1..000000000 --- a/test/Schema/propertySignature.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as AST from "@effect/schema/AST" -import * as S from "@effect/schema/Schema" -import { describe, expect, it } from "vitest" - -describe("Schema/propertySignature", () => { - it("should add annotations to propertySignature", () => { - const schema = S.struct({ - a: S.propertySignature(S.string, { - title: "title", - [Symbol.for("custom-annotation")]: "custom-annotation-value" - }) - }) - const ast: any = schema.ast - expect(ast.propertySignatures[0].annotations).toEqual({ - [AST.TitleAnnotationId]: "title", - [Symbol.for("custom-annotation")]: "custom-annotation-value" - }) - }) -}) diff --git a/test/Schema/propertySignatureAnnotations.test.ts b/test/Schema/propertySignatureAnnotations.test.ts new file mode 100644 index 000000000..7192179e2 --- /dev/null +++ b/test/Schema/propertySignatureAnnotations.test.ts @@ -0,0 +1,33 @@ +import * as AST from "@effect/schema/AST" +import * as S from "@effect/schema/Schema" +import { describe, expect, it } from "vitest" + +describe("Schema/propertySignatureAnnotations", () => { + it("should add property signature annotations to a schema", () => { + const schema = S.struct({ + a: S.string.pipe(S.propertySignatureAnnotations({ + title: "title", + [Symbol.for("custom-annotation")]: "custom-annotation-value" + })) + }) + const ast: any = schema.ast + expect(ast.propertySignatures[0].annotations).toEqual({ + [AST.TitleAnnotationId]: "title", + [Symbol.for("custom-annotation")]: "custom-annotation-value" + }) + }) + + it("should add property signature annotations to a property signature", () => { + const schema = S.struct({ + a: S.optional(S.string).pipe(S.propertySignatureAnnotations({ + title: "title", + [Symbol.for("custom-annotation")]: "custom-annotation-value" + })) + }) + const ast: any = schema.ast + expect(ast.propertySignatures[0].annotations).toEqual({ + [AST.TitleAnnotationId]: "title", + [Symbol.for("custom-annotation")]: "custom-annotation-value" + }) + }) +}) diff --git a/test/Schema/required.test.ts b/test/Schema/required.test.ts index 58fd260a2..b045b052e 100644 --- a/test/Schema/required.test.ts +++ b/test/Schema/required.test.ts @@ -12,7 +12,7 @@ describe("Schema/required", () => { it("struct", async () => { const schema = S.required(S.struct({ - a: S.optional(NumberFromString.pipe(S.greaterThan(0))) + a: S.optional(NumberFromString.pipe(S.greaterThan(0)), { exact: true }) })) await Util.expectParseSuccess(schema, { a: "1" }, { a: 1 }) @@ -79,8 +79,8 @@ describe("Schema/required", () => { it("union", async () => { const schema = S.required(S.union( - S.struct({ a: S.optional(S.string) }), - S.struct({ b: S.optional(S.number) }) + S.struct({ a: S.optional(S.string, { exact: true }) }), + S.struct({ b: S.optional(S.number, { exact: true }) }) )) await Util.expectParseSuccess(schema, { a: "a" }) await Util.expectParseSuccess(schema, { b: 1 }) @@ -98,7 +98,7 @@ describe("Schema/required", () => { const schema: S.Schema = S.required(S.suspend( // intended outer suspend () => S.struct({ - a: S.optional(S.union(S.null, schema)) + a: S.optional(S.union(S.null, schema), { exact: true }) }) )) await Util.expectParseSuccess(schema, { a: null }) diff --git a/test/Schema/struct.test.ts b/test/Schema/struct.test.ts index 87c30cab5..d9a55d2a1 100644 --- a/test/Schema/struct.test.ts +++ b/test/Schema/struct.test.ts @@ -85,7 +85,7 @@ describe("Schema/struct", () => { }) it("optional property signature", async () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) await Util.expectParseSuccess(schema, {}) await Util.expectParseSuccess(schema, { a: 1 }) @@ -113,7 +113,7 @@ describe("Schema/struct", () => { }) it("optional property signature with undefined", async () => { - const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined)) }) + const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) await Util.expectParseSuccess(schema, {}) await Util.expectParseSuccess(schema, { a: 1 }) await Util.expectParseSuccess(schema, { a: undefined }) @@ -137,7 +137,10 @@ describe("Schema/struct", () => { }) it("should not add optional keys", async () => { - const schema = S.struct({ a: S.optional(S.string), b: S.optional(S.number) }) + const schema = S.struct({ + a: S.optional(S.string, { exact: true }), + b: S.optional(S.number, { exact: true }) + }) await Util.expectParseSuccess(schema, {}) }) }) @@ -180,7 +183,7 @@ describe("Schema/struct", () => { }) it("optional property signature", async () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) await Util.expectEncodeSuccess(schema, {}, {}) await Util.expectEncodeSuccess(schema, { a: 1 }, { a: 1 }) await Util.expectEncodeFailure( @@ -192,7 +195,7 @@ describe("Schema/struct", () => { }) it("optional property signature with undefined", async () => { - const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined)) }) + const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) await Util.expectEncodeSuccess(schema, {}, {}) await Util.expectEncodeSuccess(schema, { a: 1 }, { a: 1 }) await Util.expectEncodeSuccess(schema, { a: undefined }, { a: undefined }) diff --git a/test/Schema/union.test.ts b/test/Schema/union.test.ts index 670700413..26b3761a3 100644 --- a/test/Schema/union.test.ts +++ b/test/Schema/union.test.ts @@ -81,8 +81,8 @@ describe("Schema/literal", () => { }) it("union/optional property signatures: should return the best output", async () => { - const ab = S.struct({ a: S.string, b: S.optional(S.number) }) - const ac = S.struct({ a: S.string, c: S.optional(S.number) }) + const ab = S.struct({ a: S.string, b: S.optional(S.number, { exact: true }) }) + const ac = S.struct({ a: S.string, c: S.optional(S.number, { exact: true }) }) const schema = S.union(ab, ac) await Util.expectParseSuccess( schema, @@ -116,8 +116,8 @@ describe("Schema/literal", () => { }) it("union/ optional property signatures", async () => { - const ab = S.struct({ a: S.string, b: S.optional(S.number) }) - const ac = S.struct({ a: S.string, c: S.optional(S.number) }) + const ab = S.struct({ a: S.string, b: S.optional(S.number, { exact: true }) }) + const ac = S.struct({ a: S.string, c: S.optional(S.number, { exact: true }) }) const schema = S.union(ab, ac) await Util.expectEncodeSuccess( schema, diff --git a/test/TreeFormatter.test.ts b/test/TreeFormatter.test.ts index 02e641ced..965e517cb 100644 --- a/test/TreeFormatter.test.ts +++ b/test/TreeFormatter.test.ts @@ -53,7 +53,8 @@ describe("formatErrors", () => { S.union( S.struct({ type: S.literal("f"), f: S.string }), S.struct({ type: S.literal("g"), g: S.number }) - ) + ), + { exact: true } ) }) await Util.expectParseFailureTree( diff --git a/test/Uint8Array/Uint8ArrayFromSelf.test.ts b/test/Uint8Array/Uint8ArrayFromSelf.test.ts index f52d5cd4f..984c81eca 100644 --- a/test/Uint8Array/Uint8ArrayFromSelf.test.ts +++ b/test/Uint8Array/Uint8ArrayFromSelf.test.ts @@ -4,10 +4,6 @@ import * as Util from "@effect/schema/test/util" import { describe, expect, it } from "vitest" describe("Uint8Array/Uint8ArrayFromSelf", () => { - it("keyof", () => { - expect(S.keyof(S.Uint8ArrayFromSelf)).toEqual(S.never) - }) - it("property tests", () => { Util.roundtrip(S.Uint8ArrayFromSelf) }) diff --git a/test/compiler/TypeScript.test.ts b/test/compiler/TypeScript.test.ts index 6c0caaf1c..95a084009 100644 --- a/test/compiler/TypeScript.test.ts +++ b/test/compiler/TypeScript.test.ts @@ -704,7 +704,7 @@ describe("TypeScript", () => { }) it("optional property signature", () => { - const schema = S.struct({ a: S.optional(S.number) }) + const schema = S.struct({ a: S.optional(S.number, { exact: true }) }) const ts = typeScriptFor(schema) expect(printNodes(ts.nodes)).toEqual([`{ readonly a?: number; @@ -712,7 +712,7 @@ describe("TypeScript", () => { }) it("optional property signature with undefined", () => { - const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined)) }) + const schema = S.struct({ a: S.optional(S.union(S.number, S.undefined), { exact: true }) }) const ts = typeScriptFor(schema) expect(printNodes(ts.nodes)).toEqual([`{ readonly a?: number | undefined;