diff --git a/.changeset/two-bananas-mix.md b/.changeset/two-bananas-mix.md new file mode 100644 index 0000000000..7150f87283 --- /dev/null +++ b/.changeset/two-bananas-mix.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add Either.liftPredicate diff --git a/packages/effect/dtslint/Either.ts b/packages/effect/dtslint/Either.ts index 30609ee059..f2d240b442 100644 --- a/packages/effect/dtslint/Either.ts +++ b/packages/effect/dtslint/Either.ts @@ -110,6 +110,80 @@ string$string.pipe(Either.andThen(string$number)) // $ExpectType Either string$string.pipe(Either.andThen(() => string$number)) +// ------------------------------------------------------------------------------------- +// liftPredicate +// ------------------------------------------------------------------------------------- + +declare const primitiveNumber: number +declare const primitiveNumberOrString: string | number +declare const predicateNumbersOrStrings: Predicate.Predicate + +// $ExpectType Either +pipe( + primitiveNumberOrString, + Either.liftPredicate(Predicate.isString, ( + _s // $ExpectType string | number + ) => "b" as const) +) + +// $ExpectType Either +Either.liftPredicate(primitiveNumberOrString, Predicate.isString, ( + _s // $ExpectType string | number +) => "b" as const) + +// $ExpectType Either +pipe( + primitiveNumberOrString, + Either.liftPredicate( + ( + n // $ExpectType string | number + ): n is number => typeof n === "number", + ( + _s // $ExpectType string | number + ) => "b" as const + ) +) + +// $ExpectType Either +Either.liftPredicate( + primitiveNumberOrString, + ( + n // $ExpectType string | number + ): n is number => typeof n === "number", + ( + _s // $ExpectType string | number + ) => "b" as const +) + +// $ExpectType Either +pipe( + primitiveNumberOrString, + Either.liftPredicate(predicateNumbersOrStrings, ( + _s // $ExpectType string | number + ) => "b" as const) +) + +// $ExpectType Either +pipe( + primitiveNumber, + Either.liftPredicate(predicateNumbersOrStrings, ( + _s // $ExpectType number + ) => "b" as const) +) + +// $ExpectType Either +pipe( + primitiveNumber, + Either.liftPredicate( + ( + _n // $ExpectType number + ) => true, + ( + _s // $ExpectType number + ) => "b" as const + ) +) + // ------------------------------------------------------------------------------------- // filterOrLeft // ------------------------------------------------------------------------------------- diff --git a/packages/effect/src/Either.ts b/packages/effect/src/Either.ts index fa789efeed..2be18c7aeb 100644 --- a/packages/effect/src/Either.ts +++ b/packages/effect/src/Either.ts @@ -389,6 +389,57 @@ export const match: { }): B | C => isLeft(self) ? onLeft(self.left) : onRight(self.right) ) +/** + * Transforms a `Predicate` function into a `Right` of the input value if the predicate returns `true` + * or `Left` of the result of the provided function if the predicate returns false + * + * @param predicate - A `Predicate` function that takes in a value of type `A` and returns a boolean. + * + * @example + * import { pipe, Either } from "effect" + * + * const isPositive = (n: number): boolean => n > 0 + * + * assert.deepStrictEqual( + * pipe( + * 1, + * Either.liftPredicate(isPositive, n => `${n} is not positive`) + * ), + * Either.right(1) + * ) + * assert.deepStrictEqual( + * pipe( + * 0, + * Either.liftPredicate(isPositive, n => `${n} is not positive`) + * ), + * Either.left("0 is not positive") + * ) + * + * @category lifting + * @since 3.4.0 + */ +export const liftPredicate: { + (refinement: Refinement, B>, orLeftWith: (a: NoInfer) => E): (a: A) => Either + ( + predicate: Predicate>, + orLeftWith: (a: NoInfer) => E + ): (a: A) => Either + ( + self: A, + refinement: Refinement, + orLeftWith: (a: A) => E + ): Either + ( + self: A, + predicate: Predicate>, + orLeftWith: (a: NoInfer) => E + ): Either +} = dual( + 3, + (a: A, predicate: Predicate, orLeftWith: (a: A) => E): Either => + predicate(a) ? right(a) : left(orLeftWith(a)) +) + /** * Filter the right value with the provided function. * If the predicate fails, set the left value with the result of the provided function. diff --git a/packages/effect/test/Either.test.ts b/packages/effect/test/Either.test.ts index 290b041950..092b3741d0 100644 --- a/packages/effect/test/Either.test.ts +++ b/packages/effect/test/Either.test.ts @@ -173,6 +173,47 @@ describe("Either", () => { Util.deepStrictEqual(Either.flip(Either.left("b")), Either.right("b")) }) + it("liftPredicate", () => { + const isPositivePredicate = (n: number) => n > 0 + const onPositivePredicateError = (n: number) => `${n} is not positive` + const isNumberRefinement = (n: string | number): n is number => typeof n === "number" + const onNumberRefinementError = (n: string | number) => `${n} is not a number` + + Util.deepStrictEqual( + pipe(1, Either.liftPredicate(isPositivePredicate, onPositivePredicateError)), + Either.right(1) + ) + Util.deepStrictEqual( + pipe(-1, Either.liftPredicate(isPositivePredicate, onPositivePredicateError)), + Either.left(`-1 is not positive`) + ) + Util.deepStrictEqual( + pipe(1, Either.liftPredicate(isNumberRefinement, onNumberRefinementError)), + Either.right(1) + ) + Util.deepStrictEqual( + pipe("string", Either.liftPredicate(isNumberRefinement, onNumberRefinementError)), + Either.left(`string is not a number`) + ) + + Util.deepStrictEqual( + Either.liftPredicate(1, isPositivePredicate, onPositivePredicateError), + Either.right(1) + ) + Util.deepStrictEqual( + Either.liftPredicate(-1, isPositivePredicate, onPositivePredicateError), + Either.left(`-1 is not positive`) + ) + Util.deepStrictEqual( + Either.liftPredicate(1, isNumberRefinement, onNumberRefinementError), + Either.right(1) + ) + Util.deepStrictEqual( + Either.liftPredicate("string", isNumberRefinement, onNumberRefinementError), + Either.left(`string is not a number`) + ) + }) + it("filterOrLeft", () => { Util.deepStrictEqual(Either.filterOrLeft(Either.right(1), (n) => n > 0, () => "a"), Either.right(1)) Util.deepStrictEqual(Either.filterOrLeft(Either.right(1), (n) => n > 1, () => "a"), Either.left("a"))