diff --git a/.changeset/quiet-ladybugs-fly.md b/.changeset/quiet-ladybugs-fly.md new file mode 100644 index 0000000000..d6561e7394 --- /dev/null +++ b/.changeset/quiet-ladybugs-fly.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +support $is & $match for Data.TaggedEnum with generics diff --git a/packages/effect/src/Data.ts b/packages/effect/src/Data.ts index 1554b33ca6..dacabf0590 100644 --- a/packages/effect/src/Data.ts +++ b/packages/effect/src/Data.ts @@ -7,6 +7,7 @@ import * as internal from "./internal/data.js" import { StructuralPrototype } from "./internal/effectable.js" import * as Predicate from "./Predicate.js" import type * as Types from "./Types.js" +import type { Unify } from "./Unify.js" /** * @since 2.0.0 @@ -336,13 +337,63 @@ export declare namespace TaggedEnum { } & { readonly $is: (tag: Tag) => (u: unknown) => u is Extract - readonly $match: < + readonly $match: { + < + Cases extends { + readonly [Tag in A["_tag"]]: (args: Extract) => any + } + >(cases: Cases): (value: A) => Unify> + < + Cases extends { + readonly [Tag in A["_tag"]]: (args: Extract) => any + } + >(value: A, cases: Cases): Unify> + } + } + > + + /** + * @since 3.2.0 + */ + export interface GenericMatchers> { + readonly $is: ( + tag: Tag + ) => { + >( + u: T + ): u is T & { readonly _tag: Tag } + (u: unknown): u is Extract, { readonly _tag: Tag }> + } + readonly $match: { + < + A, + B, + C, + D, + Cases extends { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract, { readonly _tag: Tag }> + ) => any + } + >( + cases: Cases + ): (self: TaggedEnum.Kind) => Unify> + < + A, + B, + C, + D, Cases extends { - readonly [Tag in A["_tag"]]: (args: Extract) => any + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: Extract, { readonly _tag: Tag }> + ) => any } - >(cases: Cases) => (value: A) => ReturnType + >( + self: TaggedEnum.Kind, + cases: Cases + ): Unify> } - > + } } /** @@ -379,52 +430,60 @@ export declare namespace TaggedEnum { * @since 2.0.0 */ export const taggedEnum: { - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > - >(): { - readonly [Tag in Z["taggedEnum"]["_tag"]]: ( - args: TaggedEnum.Args< - TaggedEnum.Kind, - Tag, - Extract, { readonly _tag: Tag }> - > - ) => TaggedEnum.Value, Tag> - } + >(): Types.Simplify< + { + readonly [Tag in Z["taggedEnum"]["_tag"]]: ( + args: TaggedEnum.Args< + TaggedEnum.Kind, + Tag, + Extract, { readonly _tag: Tag }> + > + ) => TaggedEnum.Value, Tag> + } & TaggedEnum.GenericMatchers + > (): TaggedEnum.Constructor } = () => new Proxy({}, { get(_target, tag, _receiver) { if (tag === "$is") { - return taggedIs + return Predicate.isTagged } else if (tag === "$match") { return taggedMatch } @@ -432,19 +491,33 @@ export const taggedEnum: { } }) as any -function taggedIs(tag: Tag) { - return Predicate.isTagged(tag) -} - function taggedMatch< A extends { readonly _tag: string }, Cases extends { readonly [K in A["_tag"]]: (args: Extract) => any } ->(cases: Cases) { - return function(value: A): ReturnType { - return cases[value._tag as A["_tag"]](value as any) +>(self: A, cases: Cases): ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(cases: Cases): (value: A) => ReturnType +function taggedMatch< + A extends { readonly _tag: string }, + Cases extends { + readonly [K in A["_tag"]]: (args: Extract) => any + } +>(): any { + if (arguments.length === 1) { + const cases = arguments[0] as Cases + return function(value: A): ReturnType { + return cases[value._tag as A["_tag"]](value as any) + } } + const value = arguments[0] as A + const cases = arguments[1] as Cases + return cases[value._tag as A["_tag"]](value as any) } /** diff --git a/packages/effect/test/Data.test.ts b/packages/effect/test/Data.test.ts index 974c91bfe7..e7a1f551c5 100644 --- a/packages/effect/test/Data.test.ts +++ b/packages/effect/test/Data.test.ts @@ -1,6 +1,7 @@ import * as Data from "effect/Data" import * as Equal from "effect/Equal" -import { describe, expect, it } from "vitest" +import { pipe } from "effect/Function" +import { assert, describe, expect, it } from "vitest" describe("Data", () => { it("struct", () => { @@ -218,7 +219,7 @@ describe("Data", () => { interface ResultDefinition extends Data.TaggedEnum.WithGenerics<2> { readonly taggedEnum: Result } - const { Failure, Success } = Data.taggedEnum() + const { $is, $match, Failure, Success } = Data.taggedEnum() const a = Success({ value: 1 }) satisfies Result const b = Failure({ error: "test" }) satisfies Result @@ -233,6 +234,34 @@ describe("Data", () => { expect(Equal.equals(a, b)).toBe(false) expect(Equal.equals(a, c)).toBe(true) + + const aResult = Success({ value: 1 }) as Result + const bResult = Failure({ error: "boom" }) as Result + + assert.strictEqual( + $match(aResult, { + Success: (_) => 1, + Failure: (_) => 2 + }), + 1 + ) + const result = pipe( + bResult, + $match({ + Success: (_) => _.value, + Failure: (_) => _.error + }) + ) + result satisfies string | number + assert.strictEqual(result, "boom") + + assert($is("Success")(aResult)) + aResult satisfies { readonly _tag: "Success"; readonly value: number } + assert.strictEqual(aResult.value, 1) + + assert($is("Failure")(bResult)) + bResult satisfies { readonly _tag: "Failure"; readonly error: string } + assert.strictEqual(bResult.error, "boom") }) describe("Error", () => {