An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix tapBoth to use independent type params per handler and update changelog

+80 -8
+25
CHANGELOG.md
··· 2 2 3 3 All notable changes to this project will be documented in this file. 4 4 5 + ## [0.1.0-alpha.11] - 2026-02-28 6 + 7 + ### Added 8 + 9 + - **`tapEffect`** — Effectful success tap: observe a success value with an effect-returning side-effect, discard the result, return the original value. Errors from the side-effect propagate into the error union. 10 + - **`tapError`** — Effectful error tap: observe an error with an effect-returning side-effect, discard the result, re-raise the original error. 11 + - **`tapBoth`** — Effectful dual tap: observe both success and failure with independent effectful handlers. Each handler can have its own error and requirement types. 12 + 13 + ### Changed 14 + 15 + - **`tapBoth` handlers have independent type params** — `onSuccess` and `onFailure` can use different error (`E2`/`E3`) and requirement (`R2`/`R3`) types, enabling different effect types per branch. 16 + 17 + ## [0.1.0-alpha.10] - 2026-02-28 18 + 19 + ### Fixed 20 + 21 + - Type errors in examples (stories, task-queue, user-registration) 22 + - `isSet` / `isMap` guards now return `ReadonlySet` / `ReadonlyMap` for stricter type safety 23 + - `and` guard combinator returns intersection type (`B & C`) instead of requiring `C extends B` 24 + 25 + ### Changed 26 + 27 + - Expanded guard tests (137+ assertions covering edge cases) 28 + - Added `tsconfig.test.json` path mappings 29 + 5 30 ## [0.1.0-alpha.9] - 2026-02-07 6 31 7 32 ### Added
+9 -8
src/effect/combinators.ts
··· 147 147 /** 148 148 * Observe both success and failure with effectful side-effects. 149 149 * The original value or error is always preserved after the side-effect runs. 150 + * Each handler can have independent error and requirement types. 150 151 * 151 152 * @example 152 153 * ```typescript ··· 160 161 * ``` 161 162 */ 162 163 export const tapBoth = 163 - <A, E, E2, R2>(handlers: { 164 + <A, E, E2, R2, E3, R3>(handlers: { 164 165 readonly onSuccess: (a: A) => Eff<unknown, E2, R2> 165 - readonly onFailure: (e: E) => Eff<unknown, E2, R2> 166 + readonly onFailure: (e: E) => Eff<unknown, E3, R3> 166 167 }) => 167 - <R>(eff: Eff<A, E, R>): Eff<A, E | E2, R | R2> => 168 + <R>(eff: Eff<A, E, R>): Eff<A, E | E2 | E3, R | R2 | R3> => 168 169 pipe( 169 - eff as Eff<A, E | E2, R | R2>, 170 + eff as Eff<A, E | E2 | E3, R | R2 | R3>, 170 171 foldEff( 171 - (e: E | E2) => 172 + (e: E | E2 | E3) => 172 173 pipe( 173 - handlers.onFailure(e as E) as Eff<unknown, E | E2, R | R2>, 174 - flatMap(() => fail(e) as Eff<A, E | E2, R | R2>), 174 + handlers.onFailure(e as E) as Eff<unknown, E | E2 | E3, R | R2 | R3>, 175 + flatMap(() => fail(e) as Eff<A, E | E2 | E3, R | R2 | R3>), 175 176 ), 176 177 (a: A) => 177 178 pipe( 178 - handlers.onSuccess(a) as Eff<unknown, E | E2, R | R2>, 179 + handlers.onSuccess(a) as Eff<unknown, E | E2 | E3, R | R2 | R3>, 179 180 mapEff(() => a), 180 181 ), 181 182 ),
+46
tests/effect-basic.test.ts
··· 382 382 expect(result).toBe(20) 383 383 expect(log).toEqual(["first:10", "second:20"]) 384 384 }) 385 + 386 + it("works with async side-effects", async () => { 387 + let observed: number | null = null 388 + const result = await runPromise( 389 + pipe( 390 + succeed(42), 391 + tapEffect((a: number) => 392 + fromPromise(() => 393 + Promise.resolve().then(() => { 394 + observed = a 395 + }), 396 + ), 397 + ), 398 + ), 399 + ) 400 + expect(result).toBe(42) 401 + expect(observed as unknown as number).toBe(42) 402 + }) 385 403 }) 386 404 387 405 describe("tapError (effectful)", () => { ··· 502 520 ) 503 521 expect(Exit.isFailure(exit)).toBe(true) 504 522 if (Exit.isFailure(exit)) expect(exit.error).toBe("original-err") 523 + }) 524 + 525 + it("when onSuccess side-effect fails, that error propagates", async () => { 526 + const exit = await runPromiseExit( 527 + pipe( 528 + succeed(42), 529 + tapBoth({ 530 + onSuccess: (_a: number) => fail("side-effect-error"), 531 + onFailure: (_e: string) => succeed(undefined), 532 + }), 533 + ), 534 + ) 535 + expect(Exit.isFailure(exit)).toBe(true) 536 + if (Exit.isFailure(exit)) expect(exit.error).toBe("side-effect-error") 537 + }) 538 + 539 + it("when onFailure side-effect fails, that error propagates", async () => { 540 + const exit = await runPromiseExit( 541 + pipe( 542 + fail("original") as Eff<number, string, unknown>, 543 + tapBoth({ 544 + onSuccess: (_a: number) => succeed(undefined), 545 + onFailure: (_e: string) => fail("side-effect-error"), 546 + }), 547 + ), 548 + ) 549 + expect(Exit.isFailure(exit)).toBe(true) 550 + if (Exit.isFailure(exit)) expect(exit.error).toBe("side-effect-error") 505 551 }) 506 552 }) 507 553