Skip to content

fix(Data): preserve generic A in TaggedEnum $match arms#6250

Open
milkyskies wants to merge 1 commit into
Effect-TS:mainfrom
milkyskies:fix/taggedenum-generic-match-inference
Open

fix(Data): preserve generic A in TaggedEnum $match arms#6250
milkyskies wants to merge 1 commit into
Effect-TS:mainfrom
milkyskies:fix/taggedenum-generic-match-inference

Conversation

@milkyskies
Copy link
Copy Markdown
Contributor

@milkyskies milkyskies commented May 27, 2026

Type

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

The direct-form $match(self, cases) overload on Data.TaggedEnum's GenericMatchers had A, B, C, D as explicit type parameters, with cases constrained against TaggedEnum.Kind<Z, A, B, C, D>. TypeScript infers Cases and the type params in a single pass; when the caller is itself a generic function (e.g. function f<A>(node: Tree<A>)), A is unbound at inference time and defaults to its constraint, so arms receive unknown instead of A:

function collectValues<A>(node: Tree<A>): ReadonlyArray<A> {
  return Tree.$match(node, {
    Leaf: ({ value }) => [value],
    //     ^^^^^ inferred as `unknown`, expected `A`
    Branch: ({ children }) => children.flatMap(collectValues),
  });
  // Type 'unknown[]' is not assignable to type 'readonly A[]'.
}

Match.value(node).pipe(Match.tag(...)) works because Match.value<const I>(i: I) captures I as the concrete Tree<A> instance before any cases are typed.

Fix

Reorder the two $match overloads so the direct form comes first, and rewrite the direct form to infer a single const Self extends Kind<Z, any, any, any, any> from self, then derive Cases from Self. The generic A flows in through Self via const inference, and arms get the expected concrete arg type.

The curried form ($match(cases)(self)) is unchanged — it still works for pipe + concrete types as before.

Related

Closes Effect-TS#6249.

The direct-form $match(self, cases) overload had A, B, C, D as explicit
type parameters, with cases constrained against TaggedEnum.Kind<Z, A, B,
C, D>. TypeScript infers Cases and the type params in one pass; when the
caller is a generic function (e.g. function f<A>(node: Tree<A>)), A is
unbound at inference time and defaults to its constraint, so arms receive
unknown instead of A.

Reorder the overloads (direct form first) and rewrite the direct form to
infer a single const Self extends Kind<Z, any, any, any, any> from self,
then derive Cases from Self. The generic A flows in through Self via
const inference, and arms get the expected concrete arg type.

The curried form (cases-first) is unchanged.
@milkyskies milkyskies requested a review from mikearnaldi as a code owner May 27, 2026 05:18
@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog May 27, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 27, 2026

🦋 Changeset detected

Latest commit: 7d05d86

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
effect Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another 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

Status: Discussion Ongoing

Development

Successfully merging this pull request may close these issues.

Data.taggedEnum<WithGenerics<1>>().$match loses generic A inside arms

1 participant