From b812a79c73b3486bfaebee823225de1d04630a30 Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Tue, 11 Feb 2025 18:15:48 +0100 Subject: [PATCH] Schedule: fix unsafe `tapOutput` signature --- .changeset/gold-jobs-love.md | 33 ++++++++++++++++++++++++ packages/effect/dtslint/Schedule.ts | 33 ++++++++++++++++++++++++ packages/effect/src/Schedule.ts | 15 ++++++----- packages/effect/src/internal/schedule.ts | 28 ++++++++++++-------- packages/effect/test/Schedule.test.ts | 14 ++++++++++ 5 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 .changeset/gold-jobs-love.md create mode 100644 packages/effect/dtslint/Schedule.ts diff --git a/.changeset/gold-jobs-love.md b/.changeset/gold-jobs-love.md new file mode 100644 index 0000000000..e561705029 --- /dev/null +++ b/.changeset/gold-jobs-love.md @@ -0,0 +1,33 @@ +--- +"effect": patch +--- + +Schedule: fix unsafe `tapOutput` signature. + +Previously, `tapOutput` allowed using an output type that wasn't properly inferred, leading to potential runtime errors. Now, TypeScript correctly detects mismatches at compile time, preventing unexpected crashes. + +**Before (Unsafe, Causes Runtime Error)** + +```ts +import { Effect, Schedule, Console } from "effect" + +const schedule = Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((s: string) => Console.log(s.trim())) // ❌ Runtime error +) + +Effect.runPromise(Effect.void.pipe(Effect.schedule(schedule))) +// throws: TypeError: s.trim is not a function +``` + +**After (Safe, Catches Type Error at Compile Time)** + +```ts +import { Console, Schedule } from "effect" + +const schedule = Schedule.once.pipe( + Schedule.as(1), + // ✅ Type Error: Type 'number' is not assignable to type 'string' + Schedule.tapOutput((s: string) => Console.log(s.trim())) +) +``` diff --git a/packages/effect/dtslint/Schedule.ts b/packages/effect/dtslint/Schedule.ts new file mode 100644 index 0000000000..324360cf5f --- /dev/null +++ b/packages/effect/dtslint/Schedule.ts @@ -0,0 +1,33 @@ +import { Console, Schedule } from "effect" + +// ------------------------------------------------------------------------------------- +// tapOutput +// ------------------------------------------------------------------------------------- + +// $ExpectType Schedule +Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput(( + x // $ExpectType string | number + ) => Console.log(x)) +) + +// The callback should not affect the type of the output (`number`) +// $ExpectType Schedule +Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((x: string | number) => Console.log(x)) +) +// $ExpectType Schedule +Schedule.tapOutput( + Schedule.once.pipe( + Schedule.as(1) + ), + (x: string | number) => Console.log(x) +) + +Schedule.once.pipe( + Schedule.as(1), + // @ts-expect-error + Schedule.tapOutput((s: string) => Console.log(s.trim())) +) diff --git a/packages/effect/src/Schedule.ts b/packages/effect/src/Schedule.ts index 1010e3962b..fe42b14b22 100644 --- a/packages/effect/src/Schedule.ts +++ b/packages/effect/src/Schedule.ts @@ -1763,7 +1763,8 @@ export const spaced: (duration: Duration.DurationInput) => Schedule = in export const stop: Schedule = internal.stop /** - * Returns a schedule that runs once and produces the specified constant value. + * Returns a schedule that recurs indefinitely, always producing the specified + * constant value. * * @since 2.0.0 * @category Constructors @@ -1771,8 +1772,8 @@ export const stop: Schedule = internal.stop export const succeed: (value: A) => Schedule = internal.succeed /** - * Returns a schedule that runs once, evaluating the given function to produce a - * constant value. + * Returns a schedule that recurs indefinitely, evaluating the given function to + * produce a constant value. * * @category Constructors * @since 2.0.0 @@ -1816,12 +1817,12 @@ export const tapInput: { * @category Tapping */ export const tapOutput: { - ( - f: (out: XO) => Effect.Effect + ( + f: (out: Types.NoInfer) => Effect.Effect ): (self: Schedule) => Schedule - ( + ( self: Schedule, - f: (out: XO) => Effect.Effect + f: (out: Out) => Effect.Effect ): Schedule } = internal.tapOutput diff --git a/packages/effect/src/internal/schedule.ts b/packages/effect/src/internal/schedule.ts index 9d980fdc3f..25d875f94a 100644 --- a/packages/effect/src/internal/schedule.ts +++ b/packages/effect/src/internal/schedule.ts @@ -1362,19 +1362,25 @@ export const tapInput = dual< /** @internal */ export const tapOutput = dual< - ( - f: (out: XO) => Effect.Effect - ) => (self: Schedule.Schedule) => Schedule.Schedule, - ( + ( + f: (out: Types.NoInfer) => Effect.Effect + ) => (self: Schedule.Schedule) => Schedule.Schedule, + ( self: Schedule.Schedule, - f: (out: XO) => Effect.Effect + f: (out: Out) => Effect.Effect ) => Schedule.Schedule ->(2, (self, f) => - makeWithState(self.initial, (now, input, state) => - core.tap( - self.step(now, input, state), - ([, out]) => f(out as any) - ))) +>( + 2, + ( + self: Schedule.Schedule, + f: (out: Out) => Effect.Effect + ): Schedule.Schedule => + makeWithState(self.initial, (now, input, state) => + core.tap( + self.step(now, input, state), + ([, out]) => f(out) + )) +) /** @internal */ export const unfold = (initial: A, f: (a: A) => A): Schedule.Schedule => diff --git a/packages/effect/test/Schedule.test.ts b/packages/effect/test/Schedule.test.ts index 49840c842b..98cab88341 100644 --- a/packages/effect/test/Schedule.test.ts +++ b/packages/effect/test/Schedule.test.ts @@ -814,6 +814,20 @@ describe("Schedule", () => { ) deepStrictEqual(exit, Exit.die(exception)) })) + it.effect("tapOutput", () => + Effect.gen(function*() { + const log: Array = [] + const schedule = Schedule.once.pipe( + Schedule.as(1), + Schedule.tapOutput((x) => + Effect.sync(() => { + log.push(x) + }) + ) + ) + yield* Effect.void.pipe(Effect.schedule(schedule)) + deepStrictEqual(log, [1, 1]) + })) }) })