Skip to content

Commit

Permalink
Add refineTypeId unique symbol to the refine interface to ensure … (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jun 24, 2024
1 parent 6ddabaa commit 192261b
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-bananas-deny.md
Original file line number Diff line number Diff line change
@@ -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
56 changes: 54 additions & 2 deletions packages/schema/dtslint/Class.ts
Original file line number Diff line number Diff line change
@@ -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<Fields extends S.Struct.Fields> = S.Struct<Fields> | {
readonly [S.refineTypeId]: HasFields<Fields>
}

declare const checkForConflicts: <Fields extends S.Struct.Fields>(
fieldsOr: Fields | HasFields<Fields>
) => S.Struct<Fields>

// $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<Struct<{ a: typeof String$; }>>; }>
checkForConflicts({ fields: S.Struct({ a: S.String }).pipe(S.filter(() => true)) })

// $ExpectType Struct<{ fields: filter<filter<Struct<{ a: typeof String$; }>>>; }>
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<Struct<{ a: typeof String$; }>>; }>
checkForConflicts({ from: S.Struct({ a: S.String }).pipe(S.filter(() => true)) })

// $ExpectType Struct<{ from: filter<filter<Struct<{ a: typeof String$; }>>>; }>
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.
// ---------------------------------------------
Expand Down Expand Up @@ -149,8 +203,6 @@ hole<ConstructorParameters<typeof ExtendedFromTaggedClassFields>>()
// should accept a HasFields as argument
// ---------------------------------------------

export class FromHasFields extends S.Class<FromHasFields>("FromHasFields")({ fields: { a: S.String } }) {}

export class FromStruct extends S.Class<FromStruct>("FromStruct")(S.Struct({ a: S.String })) {}

export class FromRefinement
Expand Down
23 changes: 18 additions & 5 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3121,13 +3121,26 @@ export interface suspend<A, I, R> extends AnnotableClass<suspend<A, I, R>, A, I,
*/
export const suspend = <A, I, R>(f: () => Schema<A, I, R>): suspend<A, I, R> => 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
*/
export interface refine<A, From extends Schema.Any>
extends AnnotableClass<refine<A, From>, A, Schema.Encoded<From>, Schema.Context<From>>
{
readonly [refineTypeId]: From
readonly from: From
readonly filter: (
a: Schema.Type<From>,
Expand All @@ -3151,6 +3164,8 @@ const makeRefineClass = <From extends Schema.Any, A>(
return makeRefineClass(this.from, this.filter, mergeSchemaAnnotations(this.ast, annotations))
}

static [refineTypeId] = from

static from = from

static filter = filter
Expand Down Expand Up @@ -7020,10 +7035,8 @@ export interface Class<Self, Fields extends Struct.Fields, I, R, C, Inherited, P
>
}
type HasFields<Fields extends Struct.Fields> = {
readonly fields: Fields
} | {
readonly from: HasFields<Fields>
type HasFields<Fields extends Struct.Fields> = Struct<Fields> | {
readonly [refineTypeId]: HasFields<Fields>
}
const isField = (u: unknown) => isSchema(u) || isPropertySignature(u)
Expand All @@ -7032,7 +7045,7 @@ const isFields = <Fields extends Struct.Fields>(fields: object): fields is Field
util_.ownKeys(fields).every((key) => isField((fields as any)[key]))
const getFields = <Fields extends Struct.Fields>(hasFields: HasFields<Fields>): Fields =>
"fields" in hasFields ? hasFields.fields : getFields(hasFields.from)
"fields" in hasFields ? hasFields.fields : getFields(hasFields[refineTypeId])
const getSchemaFromFieldsOr = <Fields extends Struct.Fields>(fieldsOr: Fields | HasFields<Fields>): Schema.Any =>
isFields(fieldsOr) ? Struct(fieldsOr) : isSchema(fieldsOr) ? fieldsOr : Struct(getFields(fieldsOr))
Expand Down
8 changes: 0 additions & 8 deletions packages/schema/test/Schema/Class/Class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>("A")({ fields }) {}
Util.expectFields(A.fields, fields)
class B extends S.Class<B>("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>("A")(S.Struct(fields)) {}
Expand Down
8 changes: 0 additions & 8 deletions packages/schema/test/Schema/Class/TaggedClass.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>()("A", { fields }) {}
Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields })
class B extends S.TaggedClass<B>()("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>()("A", S.Struct(fields)) {}
Expand Down
8 changes: 0 additions & 8 deletions packages/schema/test/Schema/Class/TaggedError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>()("A", { fields }) {}
Util.expectFields(A.fields, { _tag: S.getClassTag("A"), ...fields })
class B extends S.TaggedError<B>()("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>()("A", S.Struct(fields)) {}
Expand Down
10 changes: 0 additions & 10 deletions packages/schema/test/Schema/Class/extend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>("Base")(baseFields) {}
const fields = { a: S.String, b: S.Number }
class A extends Base.extend<A>("A")({ fields }) {}
Util.expectFields(A.fields, { ...baseFields, ...fields })
class B extends Base.extend<B>("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>("Base")(baseFields) {}
Expand Down

0 comments on commit 192261b

Please sign in to comment.