From a7b4b84bd5a25f51aba922f9259c3a58c98c6a4e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Apr 2024 11:52:32 +1200 Subject: [PATCH] add Match.withReturnType api (#2568) --- .changeset/hungry-bottles-design.md | 18 +++ packages/effect/src/Match.ts | 206 +++++++++++------------- packages/effect/src/internal/matcher.ts | 163 +++++++++++-------- packages/effect/test/Match.test.ts | 44 +++++ 4 files changed, 254 insertions(+), 177 deletions(-) create mode 100644 .changeset/hungry-bottles-design.md diff --git a/.changeset/hungry-bottles-design.md b/.changeset/hungry-bottles-design.md new file mode 100644 index 0000000000..9c08514889 --- /dev/null +++ b/.changeset/hungry-bottles-design.md @@ -0,0 +1,18 @@ +--- +"effect": patch +--- + +add Match.withReturnType api + +Which can be used to constrain the return type of a match expression. + +```ts +import { Match } from "effect" + +Match.type().pipe( + Match.withReturnType(), + Match.when("foo", () => "foo"), // valid + Match.when("bar", () => 123), // type error + Match.else(() => "baz") +) +``` diff --git a/packages/effect/src/Match.ts b/packages/effect/src/Match.ts index 9d0f9c9fbd..e134cc3e5e 100644 --- a/packages/effect/src/Match.ts +++ b/packages/effect/src/Match.ts @@ -25,21 +25,22 @@ export type MatcherTypeId = typeof MatcherTypeId * @category model * @since 1.0.0 */ -export type Matcher = - | TypeMatcher - | ValueMatcher +export type Matcher = + | TypeMatcher + | ValueMatcher /** * @category model * @since 1.0.0 */ -export interface TypeMatcher extends Pipeable { +export interface TypeMatcher extends Pipeable { readonly _tag: "TypeMatcher" readonly [MatcherTypeId]: { readonly _input: Contravariant readonly _filters: Covariant readonly _remaining: Covariant readonly _result: Covariant + readonly _return: Covariant } readonly cases: ReadonlyArray add(_case: Case): TypeMatcher @@ -49,12 +50,15 @@ export interface TypeMatcher e * @category model * @since 1.0.0 */ -export interface ValueMatcher extends Pipeable { +export interface ValueMatcher + extends Pipeable +{ readonly _tag: "ValueMatcher" readonly [MatcherTypeId]: { readonly _input: Contravariant readonly _filters: Covariant readonly _result: Covariant + readonly _return: Covariant } readonly provided: Provided readonly value: Either.Either @@ -130,6 +134,15 @@ export const typeTags: () => < & { readonly [Tag in Exclude>]: never } >(fields: P) => (input: I) => Unify> = internal.typeTags +/** + * @category combinators + * @since 1.0.0 + */ +export const withReturnType: () => ( + self: Matcher +) => Ret extends ([A] extends [never] ? any : A) ? Matcher + : "withReturnType constraint does not extend Result type" = internal.withReturnType + /** * @category combinators * @since 1.0.0 @@ -137,19 +150,21 @@ export const typeTags: () => < export const when: < R, const P extends Types.PatternPrimitive | Types.PatternBase, - Fn extends (_: Types.WhenMatch) => unknown + Ret, + Fn extends (_: Types.WhenMatch) => Ret >( pattern: P, f: Fn ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>, Types.ApplyFilters>>, A | ReturnType, - Pr -> = internal.when as any + Pr, + Ret +> = internal.when /** * @category combinators @@ -157,21 +172,21 @@ export const when: < */ export const whenOr: < R, - const P extends ReadonlyArray< - Types.PatternPrimitive | Types.PatternBase - >, - Fn extends (_: Types.WhenMatch) => unknown + const P extends ReadonlyArray | Types.PatternBase>, + Ret, + Fn extends (_: Types.WhenMatch) => Ret >( ...args: [...patterns: P, f: Fn] ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>, Types.ApplyFilters>>, A | ReturnType, - Pr -> = internal.whenOr as any + Pr, + Ret +> = internal.whenOr /** * @category combinators @@ -179,24 +194,20 @@ export const whenOr: < */ export const whenAnd: < R, - const P extends ReadonlyArray< - Types.PatternPrimitive | Types.PatternBase - >, - Fn extends (_: Types.WhenMatch>) => unknown + const P extends ReadonlyArray | Types.PatternBase>, + Ret, + Fn extends (_: Types.WhenMatch>) => Ret >( ...args: [...patterns: P, f: Fn] ) => ( - self: Matcher + self: Matcher ) => Matcher< I, - Types.AddWithout>>, - Types.ApplyFilters< - I, - Types.AddWithout>> - >, + Types.AddWithout>>, + Types.ApplyFilters>>>, A | ReturnType, Pr -> = internal.whenAnd as any +> = internal.whenAnd /** * @category combinators @@ -204,20 +215,17 @@ export const whenAnd: < */ export const discriminator: ( field: D -) => & string, B>( - ...pattern: [ - first: P, - ...values: Array

, - f: (_: Extract>) => B - ] +) => & string, Ret, B extends Ret>( + ...pattern: [first: P, ...values: Array

, f: (_: Extract>) => B] ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, Types.ApplyFilters>>>, B | A, - Pr + Pr, + Ret > = internal.discriminator /** @@ -226,20 +234,18 @@ export const discriminator: ( */ export const discriminatorStartsWith: ( field: D -) => ( +) => ( pattern: P, f: (_: Extract>) => B ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, - Types.ApplyFilters< - I, - Types.AddWithout>> - >, + Types.ApplyFilters>>>, B | A, - Pr + Pr, + Ret > = internal.discriminatorStartsWith as any /** @@ -250,23 +256,21 @@ export const discriminators: ( field: D ) => < R, + Ret, P extends - & { - readonly [Tag in Types.Tags & string]?: - | ((_: Extract>) => any) - | undefined - } + & { readonly [Tag in Types.Tags & string]?: ((_: Extract>) => Ret) | undefined } & { readonly [Tag in Exclude>]: never } >( fields: P ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, Types.ApplyFilters>>>, A | ReturnType, - Pr + Pr, + Ret > = internal.discriminators /** @@ -277,58 +281,50 @@ export const discriminatorsExhaustive: ( field: D ) => < R, + Ret, P extends - & { - readonly [Tag in Types.Tags & string]: ( - _: Extract> - ) => any - } + & { readonly [Tag in Types.Tags & string]: (_: Extract>) => Ret } & { readonly [Tag in Exclude>]: never } >( fields: P ) => ( - self: Matcher -) => [Pr] extends [never] ? (u: I) => Unify> - : Unify> = internal.discriminatorsExhaustive + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = + internal.discriminatorsExhaustive /** * @category combinators * @since 1.0.0 */ -export const tag: & string, B>( - ...pattern: [ - first: P, - ...values: Array

, - f: (_: Extract>) => B - ] +export const tag: & string, Ret, B extends Ret>( + ...pattern: [first: P, ...values: Array

, f: (_: Extract>) => B] ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, Types.ApplyFilters>>>, B | A, - Pr + Pr, + Ret > = internal.tag /** * @category combinators * @since 1.0.0 */ -export const tagStartsWith: ( +export const tagStartsWith: ( pattern: P, f: (_: Extract>) => B ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, - Types.ApplyFilters< - I, - Types.AddWithout>> - >, + Types.ApplyFilters>>>, B | A, - Pr + Pr, + Ret > = internal.tagStartsWith as any /** @@ -337,26 +333,21 @@ export const tagStartsWith: ( */ export const tags: < R, + Ret, P extends - & { - readonly [Tag in Types.Tags<"_tag", R> & string]?: - | ((_: Extract>) => any) - | undefined - } + & { readonly [Tag in Types.Tags<"_tag", R> & string]?: ((_: Extract>) => Ret) | undefined } & { readonly [Tag in Exclude>]: never } >( fields: P ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, - Types.ApplyFilters< - I, - Types.AddWithout>> - >, + Types.ApplyFilters>>>, A | ReturnType, - Pr + Pr, + Ret > = internal.tags /** @@ -365,19 +356,16 @@ export const tags: < */ export const tagsExhaustive: < R, + Ret, P extends - & { - readonly [Tag in Types.Tags<"_tag", R> & string]: ( - _: Extract> - ) => any - } + & { readonly [Tag in Types.Tags<"_tag", R> & string]: (_: Extract>) => Ret } & { readonly [Tag in Exclude>]: never } >( fields: P ) => ( - self: Matcher -) => [Pr] extends [never] ? (u: I) => Unify> - : Unify> = internal.tagsExhaustive + self: Matcher +) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = + internal.tagsExhaustive /** * @category combinators @@ -386,19 +374,21 @@ export const tagsExhaustive: < export const not: < R, const P extends Types.PatternPrimitive | Types.PatternBase, - Fn extends (_: Types.NotMatch) => unknown + Ret, + Fn extends (_: Exclude>>) => Ret >( pattern: P, f: Fn ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddOnly>, Types.ApplyFilters>>, A | ReturnType, - Pr -> = internal.not as any + Pr, + Ret +> = internal.not /** * @category predicates @@ -506,44 +496,42 @@ export const instanceOfUnsafe: any>( * @category conversions * @since 1.0.0 */ -export const orElse: ( +export const orElse: ( f: (b: RA) => B ) => ( - self: Matcher + self: Matcher ) => [Pr] extends [never] ? (input: I) => Unify : Unify = internal.orElse /** * @category conversions * @since 1.0.0 */ -export const orElseAbsurd: ( - self: Matcher +export const orElseAbsurd: ( + self: Matcher ) => [Pr] extends [never] ? (input: I) => Unify : Unify = internal.orElseAbsurd /** * @category conversions * @since 1.0.0 */ -export const either: ( - self: Matcher -) => [Pr] extends [never] ? (input: I) => Either.Either, R> - : Either.Either, R> = internal.either +export const either: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Either.Either, R> : Either.Either, R> = internal.either /** * @category conversions * @since 1.0.0 */ -export const option: ( - self: Matcher -) => [Pr] extends [never] ? (input: I) => Option.Option> - : Option.Option> = internal.option +export const option: ( + self: Matcher +) => [Pr] extends [never] ? (input: I) => Option.Option> : Option.Option> = internal.option /** * @category conversions * @since 1.0.0 */ -export const exhaustive: ( - self: Matcher +export const exhaustive: ( + self: Matcher ) => [Pr] extends [never] ? (u: I) => Unify : Unify = internal.exhaustive /** diff --git a/packages/effect/src/internal/matcher.ts b/packages/effect/src/internal/matcher.ts index 285f5f29f8..b9e06b8b44 100644 --- a/packages/effect/src/internal/matcher.ts +++ b/packages/effect/src/internal/matcher.ts @@ -26,7 +26,8 @@ const TypeMatcherProto: Omit, "cases"> = { _input: identity, _filters: identity, _remaining: identity, - _result: identity + _result: identity, + _return: identity }, _tag: "TypeMatcher", add( @@ -55,7 +56,8 @@ const ValueMatcherProto: Omit< [TypeId]: { _input: identity, _filters: identity, - _result: identity + _result: identity, + _return: identity }, _tag: "ValueMatcher", add( @@ -217,7 +219,7 @@ export const valueTags = < >( fields: P ) => { - const match: any = tagsExhaustive(fields)(makeTypeMatcher([])) + const match: any = tagsExhaustive(fields as any)(makeTypeMatcher([])) return (input: I): Unify> => match(input) } @@ -232,27 +234,36 @@ export const typeTags = () => >( fields: P ) => { - const match: any = tagsExhaustive(fields)(makeTypeMatcher([])) + const match: any = tagsExhaustive(fields as any)(makeTypeMatcher([])) return (input: I): Unify> => match(input) } +/** @internal */ +export const withReturnType = + () => + (self: Matcher): Ret extends ([A] extends [never] ? any + : A) ? Matcher + : "withReturnType constraint does not extend Result type" => self as any + /** @internal */ export const when = < R, const P extends Types.PatternPrimitive | Types.PatternBase, - Fn extends (_: Types.WhenMatch) => unknown + Ret, + Fn extends (_: Types.WhenMatch) => Ret >( pattern: P, f: Fn ) => ( - self: Matcher + self: Matcher ): Matcher< I, Types.AddWithout>, Types.ApplyFilters>>, A | ReturnType, - Pr + Pr, + Ret > => (self as any).add(makeWhen(makePredicate(pattern), f as any)) /** @internal */ @@ -261,18 +272,20 @@ export const whenOr = < const P extends ReadonlyArray< Types.PatternPrimitive | Types.PatternBase >, - Fn extends (_: Types.WhenMatch) => unknown + Ret, + Fn extends (_: Types.WhenMatch) => Ret >( ...args: [...patterns: P, f: Fn] ) => ( - self: Matcher + self: Matcher ): Matcher< I, Types.AddWithout>, Types.ApplyFilters>>, A | ReturnType, - Pr + Pr, + Ret > => { const onMatch = args[args.length - 1] as any const patterns = args.slice(0, -1) as unknown as P @@ -285,12 +298,13 @@ export const whenAnd = < const P extends ReadonlyArray< Types.PatternPrimitive | Types.PatternBase >, - Fn extends (_: Types.WhenMatch>) => unknown + Ret, + Fn extends (_: Types.WhenMatch>) => Ret >( ...args: [...patterns: P, f: Fn] ) => ( - self: Matcher + self: Matcher ): Matcher< I, Types.AddWithout>>, @@ -307,41 +321,43 @@ export const whenAnd = < } /** @internal */ -export const discriminator = (field: D) => - & string, B>( - ...pattern: [ - first: P, - ...values: Array

, - f: (_: Extract>) => B - ] -) => { - const f = pattern[pattern.length - 1] - const values: Array

= pattern.slice(0, -1) as any - const pred = values.length === 1 - ? (_: any) => _[field] === values[0] - : (_: any) => values.includes(_[field]) - - return ( - self: Matcher - ): Matcher< - I, - Types.AddWithout>>, - Types.ApplyFilters>>>, - A | B, - Pr - > => (self as any).add(makeWhen(pred, f as any)) as any -} +export const discriminator = + (field: D) => + & string, Ret, B extends Ret>( + ...pattern: [ + first: P, + ...values: Array

, + f: (_: Extract>) => B + ] + ) => { + const f = pattern[pattern.length - 1] + const values: Array

= pattern.slice(0, -1) as any + const pred = values.length === 1 + ? (_: any) => _[field] === values[0] + : (_: any) => values.includes(_[field]) + + return ( + self: Matcher + ): Matcher< + I, + Types.AddWithout>>, + Types.ApplyFilters>>>, + A | B, + Pr, + Ret + > => (self as any).add(makeWhen(pred, f as any)) as any + } /** @internal */ export const discriminatorStartsWith = (field: D) => -( +( pattern: P, f: (_: Extract>) => B ) => { const pred = (_: any) => typeof _[field] === "string" && _[field].startsWith(pattern) return ( - self: Matcher + self: Matcher ): Matcher< I, Types.AddWithout>>, @@ -350,7 +366,8 @@ export const discriminatorStartsWith = (field: D) => Types.AddWithout>> >, A | B, - Pr + Pr, + Ret > => (self as any).add(makeWhen(pred, f as any)) as any } @@ -358,11 +375,14 @@ export const discriminatorStartsWith = (field: D) => export const discriminators = (field: D) => < R, - P extends { - readonly [Tag in Types.Tags & string]?: ( - _: Extract> - ) => any - } + Ret, + P extends + & { + readonly [Tag in Types.Tags & string]?: + | ((_: Extract>) => Ret) + | undefined + } + & { readonly [Tag in Exclude>]: never } >( fields: P ) => { @@ -372,13 +392,14 @@ export const discriminators = (field: D) => ) return ( - self: Matcher + self: Matcher ): Matcher< I, Types.AddWithout>>, Types.ApplyFilters>>>, A | ReturnType, - Pr + Pr, + Ret > => (self as any).add(predicate) } @@ -387,15 +408,18 @@ export const discriminatorsExhaustive: ( field: D ) => < R, - P extends { - readonly [Tag in Types.Tags & string]: ( - _: Extract> - ) => any - } + Ret, + P extends + & { + readonly [Tag in Types.Tags & string]: ( + _: Extract> + ) => Ret + } + & { readonly [Tag in Exclude>]: never } >( fields: P ) => ( - self: Matcher + self: Matcher ) => [Pr] extends [never] ? (u: I) => Unify> : Unify> = (field: string) => (fields: object) => { const addCases = discriminators(field)(fields) @@ -403,20 +427,21 @@ export const discriminatorsExhaustive: ( } /** @internal */ -export const tag: & string, B>( +export const tag: & string, Ret, B extends Ret>( ...pattern: [ first: P, ...values: Array

, f: (_: Extract>) => B ] ) => ( - self: Matcher + self: Matcher ) => Matcher< I, Types.AddWithout>>, Types.ApplyFilters>>>, B | A, - Pr + Pr, + Ret > = discriminator("_tag") /** @internal */ @@ -432,19 +457,21 @@ export const tagsExhaustive = discriminatorsExhaustive("_tag") export const not = < R, const P extends Types.PatternPrimitive | Types.PatternBase, - Fn extends (_: Types.NotMatch) => unknown + Ret, + Fn extends (_: Types.NotMatch) => Ret >( pattern: P, f: Fn ) => ( - self: Matcher + self: Matcher ): Matcher< I, Types.AddOnly>, Types.ApplyFilters>>, A | ReturnType, - Pr + Pr, + Ret > => (self as any).add(makeNot(makePredicate(pattern), f as any)) /** @internal */ @@ -485,9 +512,9 @@ export const instanceOfUnsafe: any>( ) => SafeRefinement, InstanceType> = instanceOf /** @internal */ -export const orElse = (f: (b: RA) => B) => +export const orElse = (f: (b: RA) => B) => ( - self: Matcher + self: Matcher ): [Pr] extends [never] ? (input: I) => Unify : Unify => { const result = either(self) @@ -504,16 +531,16 @@ export const orElse = (f: (b: RA) => B) => } /** @internal */ -export const orElseAbsurd = ( - self: Matcher +export const orElseAbsurd = ( + self: Matcher ): [Pr] extends [never] ? (input: I) => Unify : Unify => orElse(() => { throw new Error("effect/Match/orElseAbsurd: absurd") })(self) /** @internal */ -export const either: ( - self: Matcher +export const either: ( + self: Matcher ) => [Pr] extends [never] ? (input: I) => Either.Either, R> : Either.Either, R> = ((self: Matcher) => { if (self._tag === "ValueMatcher") { @@ -547,8 +574,8 @@ export const either: ( }) as any /** @internal */ -export const option: ( - self: Matcher +export const option: ( + self: Matcher ) => [Pr] extends [never] ? (input: I) => Option.Option> : Option.Option> = ((self: Matcher) => { const toEither = either(self) @@ -568,8 +595,8 @@ export const option: ( const getExhaustiveAbsurdErrorMessage = "effect/Match/exhaustive: absurd" /** @internal */ -export const exhaustive: ( - self: Matcher +export const exhaustive: ( + self: Matcher ) => [Pr] extends [never] ? (u: I) => Unify : Unify = (( self: Matcher ) => { diff --git a/packages/effect/test/Match.test.ts b/packages/effect/test/Match.test.ts index 81a7918705..deb0f725cb 100644 --- a/packages/effect/test/Match.test.ts +++ b/packages/effect/test/Match.test.ts @@ -775,4 +775,48 @@ describe("Match", () => { expect(match(Symbol.for("a"))).toEqual("symbol") expect(match(123)).toEqual("else") }) + + it("withReturnType", () => { + const match = pipe( + M.type(), + M.withReturnType(), + M.when("A", (_) => "A"), + M.orElse(() => "else") + ) + expect(match("A")).toEqual("A") + expect(match("a")).toEqual("else") + }) + + it("withReturnType after predicate", () => { + const match = pipe( + M.type(), + M.when("A", (_) => "A"), + M.withReturnType(), + M.orElse(() => "else") + ) + expect(match("A")).toEqual("A") + expect(match("a")).toEqual("else") + }) + + it("withReturnType mismatch", () => { + const match = pipe( + M.type(), + M.withReturnType(), + // @ts-expect-error + M.when("A", (_) => 123), + M.orElse(() => "else") + ) + expect(match("A")).toEqual(123) + expect(match("a")).toEqual("else") + }) + + it("withReturnType constraint mismatch", () => { + pipe( + M.type(), + M.when("A", (_) => 123), + M.withReturnType(), + // @ts-expect-error + M.orElse(() => "else") + ) + }) })