From 192261b2aec94e9913ceed83683fdcfbc9fca66f Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Mon, 24 Jun 2024 13:15:14 +0200 Subject: [PATCH] =?UTF-8?q?Add=20`refineTypeId`=20unique=20symbol=20to=20t?= =?UTF-8?q?he=20`refine`=20interface=20to=20ensure=20=E2=80=A6=20(#3070)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/loud-bananas-deny.md | 5 ++ packages/schema/dtslint/Class.ts | 56 ++++++++++++++++++- packages/schema/src/Schema.ts | 23 ++++++-- .../schema/test/Schema/Class/Class.test.ts | 8 --- .../test/Schema/Class/TaggedClass.test.ts | 8 --- .../test/Schema/Class/TaggedError.test.ts | 8 --- .../schema/test/Schema/Class/extend.test.ts | 10 ---- 7 files changed, 77 insertions(+), 41 deletions(-) create mode 100644 .changeset/loud-bananas-deny.md diff --git a/.changeset/loud-bananas-deny.md b/.changeset/loud-bananas-deny.md new file mode 100644 index 0000000000..3b75cba18a --- /dev/null +++ b/.changeset/loud-bananas-deny.md @@ -0,0 +1,5 @@ +--- +"@effect/schema": patch +--- + +Add `refineTypeId` unique symbol to the `refine` interface to ensure correct inference of `Fields` in the Class APIs, closes #3063 diff --git a/packages/schema/dtslint/Class.ts b/packages/schema/dtslint/Class.ts index 3e3536eed0..b5b38fa48e 100644 --- a/packages/schema/dtslint/Class.ts +++ b/packages/schema/dtslint/Class.ts @@ -1,6 +1,60 @@ import * as S from "@effect/schema/Schema" import { hole } from "effect/Function" +// --------------------------------------------- +// check that there are no conflicts with the `fields` and `from` fields +// --------------------------------------------- + +type HasFields = S.Struct | { + readonly [S.refineTypeId]: HasFields +} + +declare const checkForConflicts: ( + fieldsOr: Fields | HasFields +) => S.Struct + +// $ExpectType Struct<{ fields: typeof String$; }> +checkForConflicts({ fields: S.String }) + +// $ExpectType Struct<{ from: typeof String$; }> +checkForConflicts({ from: S.String }) + +// $ExpectType Struct<{ fields: typeof String$; }> +checkForConflicts(S.Struct({ fields: S.String })) + +// $ExpectType Struct<{ from: typeof String$; }> +checkForConflicts(S.Struct({ from: S.String })) + +// $ExpectType Struct<{ fields: typeof String$; }> +checkForConflicts(S.Struct({ fields: S.String }).pipe(S.filter(() => true))) + +// $ExpectType Struct<{ from: typeof String$; }> +checkForConflicts(S.Struct({ from: S.String }).pipe(S.filter(() => true))) + +// $ExpectType Struct<{ fields: typeof String$; }> +checkForConflicts(S.Struct({ fields: S.String }).pipe(S.filter(() => true), S.filter(() => true))) + +// $ExpectType Struct<{ from: typeof String$; }> +checkForConflicts(S.Struct({ from: S.String }).pipe(S.filter(() => true), S.filter(() => true))) + +// $ExpectType Struct<{ fields: Struct<{ a: typeof String$; }>; }> +checkForConflicts({ fields: S.Struct({ a: S.String }) }) + +// $ExpectType Struct<{ fields: filter>; }> +checkForConflicts({ fields: S.Struct({ a: S.String }).pipe(S.filter(() => true)) }) + +// $ExpectType Struct<{ fields: filter>>; }> +checkForConflicts({ fields: S.Struct({ a: S.String }).pipe(S.filter(() => true), S.filter(() => true)) }) + +// $ExpectType Struct<{ from: Struct<{ a: typeof String$; }>; }> +checkForConflicts({ from: S.Struct({ a: S.String }) }) + +// $ExpectType Struct<{ from: filter>; }> +checkForConflicts({ from: S.Struct({ a: S.String }).pipe(S.filter(() => true)) }) + +// $ExpectType Struct<{ from: filter>>; }> +checkForConflicts({ from: S.Struct({ a: S.String }).pipe(S.filter(() => true), S.filter(() => true)) }) + // --------------------------------------------- // A class with no fields should permit an empty argument in the constructor. // --------------------------------------------- @@ -149,8 +203,6 @@ hole>() // should accept a HasFields as argument // --------------------------------------------- -export class FromHasFields extends S.Class("FromHasFields")({ fields: { a: S.String } }) {} - export class FromStruct extends S.Class("FromStruct")(S.Struct({ a: S.String })) {} export class FromRefinement diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 0efb701ac9..7ab22bc6d8 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -3121,6 +3121,18 @@ export interface suspend extends AnnotableClass, A, I, */ export const suspend = (f: () => Schema): suspend => make(new AST.Suspend(() => f().ast)) +/** + * @since 0.68.8 + * @category symbol + */ +export const refineTypeId: unique symbol = Symbol.for("@effect/schema/refine") + +/** + * @since 0.68.8 + * @category symbol + */ +export type refineTypeId = typeof refineTypeId + /** * @category api interface * @since 0.67.0 @@ -3128,6 +3140,7 @@ export const suspend = (f: () => Schema): suspend => export interface refine extends AnnotableClass, A, Schema.Encoded, Schema.Context> { + readonly [refineTypeId]: From readonly from: From readonly filter: ( a: Schema.Type, @@ -3151,6 +3164,8 @@ const makeRefineClass = ( return makeRefineClass(this.from, this.filter, mergeSchemaAnnotations(this.ast, annotations)) } + static [refineTypeId] = from + static from = from static filter = filter @@ -7020,10 +7035,8 @@ export interface Class } -type HasFields = { - readonly fields: Fields -} | { - readonly from: HasFields +type HasFields = Struct | { + readonly [refineTypeId]: HasFields } const isField = (u: unknown) => isSchema(u) || isPropertySignature(u) @@ -7032,7 +7045,7 @@ const isFields = (fields: object): fields is Field util_.ownKeys(fields).every((key) => isField((fields as any)[key])) const getFields = (hasFields: HasFields): Fields => - "fields" in hasFields ? hasFields.fields : getFields(hasFields.from) + "fields" in hasFields ? hasFields.fields : getFields(hasFields[refineTypeId]) const getSchemaFromFieldsOr = (fieldsOr: Fields | HasFields): Schema.Any => isFields(fieldsOr) ? Struct(fieldsOr) : isSchema(fieldsOr) ? fieldsOr : Struct(getFields(fieldsOr)) diff --git a/packages/schema/test/Schema/Class/Class.test.ts b/packages/schema/test/Schema/Class/Class.test.ts index cc2fa0d7a6..813b5ec8d3 100644 --- a/packages/schema/test/Schema/Class/Class.test.ts +++ b/packages/schema/test/Schema/Class/Class.test.ts @@ -330,14 +330,6 @@ details: Duplicate key "a"`) ) }) - it("should accept a simple object as argument", () => { - const fields = { a: S.String, b: S.Number } - class A extends S.Class("A")({ fields }) {} - Util.expectFields(A.fields, fields) - class B extends S.Class("B")({ from: { fields } }) {} - Util.expectFields(B.fields, fields) - }) - it("should accept a Struct as argument", () => { const fields = { a: S.String, b: S.Number } class A extends S.Class("A")(S.Struct(fields)) {} diff --git a/packages/schema/test/Schema/Class/TaggedClass.test.ts b/packages/schema/test/Schema/Class/TaggedClass.test.ts index cd190f0d53..4c8d8f89eb 100644 --- a/packages/schema/test/Schema/Class/TaggedClass.test.ts +++ b/packages/schema/test/Schema/Class/TaggedClass.test.ts @@ -49,14 +49,6 @@ details: Duplicate key "_tag"`) ) }) - it("should accept a simple object as argument", () => { - const fields = { a: S.String, b: S.Number } - class A extends S.TaggedClass()("A", { fields }) {} - Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields }) - class B extends S.TaggedClass()("B", { from: { fields } }) {} - Util.expectFields(B.fields, { _tag: S.getClassTag("B"), ...fields }) - }) - it("should accept a Struct as argument", () => { const fields = { a: S.String, b: S.Number } class A extends S.TaggedClass()("A", S.Struct(fields)) {} diff --git a/packages/schema/test/Schema/Class/TaggedError.test.ts b/packages/schema/test/Schema/Class/TaggedError.test.ts index 6e12a6f6b5..8c97fdd3e6 100644 --- a/packages/schema/test/Schema/Class/TaggedError.test.ts +++ b/packages/schema/test/Schema/Class/TaggedError.test.ts @@ -11,14 +11,6 @@ describe("TaggedError", () => { expect(TE._tag).toBe("TE") }) - it("should accept a simple object as argument", () => { - const fields = { a: S.String, b: S.Number } - class A extends S.TaggedError()("A", { fields }) {} - Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields }) - class B extends S.TaggedError()("B", { from: { fields } }) {} - Util.expectFields(B.fields, { _tag: S.getClassTag("B"), ...fields }) - }) - it("should accept a Struct as argument", () => { const fields = { a: S.String, b: S.Number } class A extends S.TaggedError()("A", S.Struct(fields)) {} diff --git a/packages/schema/test/Schema/Class/extend.test.ts b/packages/schema/test/Schema/Class/extend.test.ts index 19cb8df559..76b169d66c 100644 --- a/packages/schema/test/Schema/Class/extend.test.ts +++ b/packages/schema/test/Schema/Class/extend.test.ts @@ -53,16 +53,6 @@ describe("extend", () => { expect(person.nick).toEqual("Joe") }) - it("should accept a simple object as argument", () => { - const baseFields = { base: S.String } - class Base extends S.Class("Base")(baseFields) {} - const fields = { a: S.String, b: S.Number } - class A extends Base.extend("A")({ fields }) {} - Util.expectFields(A.fields, { ...baseFields, ...fields }) - class B extends Base.extend("B")({ from: { fields } }) {} - Util.expectFields(B.fields, { ...baseFields, ...fields }) - }) - it("should accept a Struct as argument", () => { const baseFields = { base: S.String } class Base extends S.Class("Base")(baseFields) {}