Skip to content

fix: restore Result-inferring andThrough overload on Err#678

Open
chatman-media wants to merge 1 commit into
supermacro:masterfrom
chatman-media:fix/err-andthrough-overload
Open

fix: restore Result-inferring andThrough overload on Err#678
chatman-media wants to merge 1 commit into
supermacro:masterfrom
chatman-media:fix/err-andthrough-overload

Conversation

@chatman-media

Copy link
Copy Markdown

Problem

Closes #663.

andThrough reports a spurious type error (and drops error types) when it is chained after a method that widens the receiver to the Result<T, E> union — e.g. a preceding .andThen():

class ErrorA extends Error { readonly _tag = 'ErrorA' as const }
class ErrorB extends Error { readonly _tag = 'ErrorB' as const }

const assertPublishable = (s: { title: string; description: string }) => {
  if (!s.title) return err(new ErrorA())
  if (!s.description) return err(new ErrorB())
  return ok(undefined)
}

ok({ title: 'title', description: 'description' })
  .andThen((s) => ok(s))      // widens receiver to Result<T, never>
  .andThrough(assertPublishable) // ❌ TS2345 before this fix

Curiously, removing the .andThen((s) => ok(s)) line makes the error disappear and the type infers correctly as Result<{ title, description }, ErrorA | ErrorB>.

Root cause

andThrough is declared with two overloads on the IResult interface and on the Ok class:

andThrough<R extends Result<unknown, unknown>>(f: (t: T) => R): Result<T, InferErrTypes<R> | E>
andThrough<F>(f: (t: T) => Result<unknown, F>): Result<T, E | F>

but the Err class only declared the second one:

andThrough<F>(_f: (t: T) => Result<unknown, F>): Result<T, E | F> { ... }

When andThrough is called on a value typed as the union Ok<T, E> | Err<T, E> (which is what Result<T, E> is, and what .andThen() produces), TypeScript resolves the call against the signatures common to both union members. Because Err was missing the first overload, resolution fell through to andThrough<F>(...), which infers F from only the first member of a union-returning callback (ErrorA) and discards the rest — producing (t) => Result<unknown, ErrorA> and the resulting mismatch.

Calling andThrough directly on an Ok (which has both overloads) already worked, which is why the error only appears once the chain has widened to Result.

Fix

Add the missing andThrough<R extends Result<unknown, unknown>> overload to the Err class so it matches Ok and the IResult interface. The runtime implementation is unchanged (Err.andThrough still just returns err(this.error)).

Tests

Added a type-level regression test in tests/typecheck-tests.ts under the andThrough block. It reproduces the issue's exact shape (a .andThen(...) followed by .andThrough(...) with a union-returning callback) and asserts the inferred type is Result<{ title, description }, ErrorA | ErrorB>. The test fails (TS2345) on master and passes with this change.

  • npm test — 117 runtime tests pass, type tests pass
  • npm run lint / npm run typecheck — clean

The Err class declared andThrough with only the
`andThrough<F>(f: (t: T) => Result<unknown, F>)` overload, while the
IResult interface and the Ok class also declare the
`andThrough<R extends Result<unknown, unknown>>` overload first.

When andThrough is called on a value typed as the Result union
(Ok<T, E> | Err<T, E>) — for example after a preceding .andThen() — TS
resolves the call against the signatures common to both members. Because
Err was missing the first overload, resolution fell through to
`andThrough<F>(...)`, which infers F from only the first member of a
union-returning callback and drops the remaining error types.

Adding the missing overload to Err makes union receivers infer the full
error union, matching the behavior already seen when calling andThrough
directly on an Ok.

Closes supermacro#663
@changeset-bot

changeset-bot Bot commented Jun 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 92b6c8e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type inference error when using .andThrough()

1 participant