Skip to content

fix(core): fix assertEvent narrowing for generic event type parameters - #5621

Open
tarann26 wants to merge 2 commits into
statelyai:mainfrom
tarann26:fix/assert-event-descriptor-typing
Open

fix(core): fix assertEvent narrowing for generic event type parameters#5621
tarann26 wants to merge 2 commits into
statelyai:mainfrom
tarann26:fix/assert-event-descriptor-typing

Conversation

@tarann26

@tarann26 tarann26 commented Aug 7, 2026

Copy link
Copy Markdown

Fixes #5448

assertEvent(event, descriptor) compiled fine when event's type was behind a generic type parameter (e.g. inside a function declared as <TEvent extends SomeEventUnion>(event: TEvent)), but every property access on event afterward failed to type-check with a "Property '...' does not exist" error. Concrete (non-generic) callers were unaffected.

Root cause: the narrowing type nests a TEvent['type'] indexed access inside a distributive conditional over TEvent. TypeScript can't eagerly resolve that nesting when TEvent is an unresolved generic parameter, so it leaves the whole narrowed type opaque and property accesses fail.

assertEvent now uses its own narrowing type that keeps the wildcard normalization and the TEvent distribution as sibling branches instead of nesting them, which TypeScript resolves independently even for generic callers. The TAssertedDescriptor extends EventDescriptor<TEvent> constraint is untouched, so invalid descriptors are still a compile-time error for both concrete and generic event types.

Added type-level regression tests in assert.types.test.ts covering the generic narrowing case from the issue, that narrowing still excludes properties from other union members (not just "does it compile"), and that invalid descriptors are still rejected for generic callers.


Open in Devin Review

assertEvent(event, descriptor) failed to narrow event when the event's
type was itself a generic type parameter (e.g. inside a function
declared as <TEvent extends SomeEventUnion>(event: TEvent)). The call
compiled, but every property access on event afterward errored with
"Property '...' does not exist", forcing callers to either avoid
generics or manually re-annotate the event's type.

This came from ExtractEvent's narrowing logic (added in the partial
descriptor support), which nests a TEvent['type'] check inside a
distributive conditional over TEvent. TypeScript can't eagerly resolve
that nesting when TEvent is an unresolved generic parameter, so it
leaves the whole narrowed type opaque.

assertEvent now uses its own narrowing type that keeps the wildcard
normalization and the TEvent distribution as sibling branches instead
of nesting them, which TypeScript can resolve independently even for
generic callers. The TAssertedDescriptor extends EventDescriptor<TEvent>
constraint is untouched, so invalid descriptors are still a compile
error for both concrete and generic event types.

Fixes statelyai#5448
@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e3613fb

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

This PR includes changesets to release 1 package
Name Type
xstate 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

devin-ai-integration[bot]

This comment was marked as resolved.

…elds

The local AssertedEvent type matched events with plain assignability
(`TEvent extends { type: Descriptor }`), which discards a member whose
`type` field is itself a union of literals: `{ type: 'a' | 'b' }` is not
assignable to `{ type: 'a' }`, so asserting 'a' collapsed the member to
`never` and property access failed. This regressed a case the original
ExtractEvent handled.

Match `{ type: infer TType }` and test the fresh TType parameter with
`true extends (TType extends Descriptor ? true : false)`, mirroring
ExtractEvent's EventDescriptorMatches. Testing an inferred parameter lets
the check distribute over a union-typed `type` field, and avoids indexing
`TEvent['type']` on an unresolved generic, so narrowing works for both
concrete and generic callers while still rejecting invalid descriptors.

Add regression tests for the union-typed `type` field case (concrete and
generic, plus precision and invalid-descriptor guards).
@tarann26

tarann26 commented Aug 7, 2026

Copy link
Copy Markdown
Author

Pushed a follow-up (e3613fb) for the union-typed type field regression flagged in review. Events like { type: 'a' | 'b'; x } were being dropped because AssertedEvent used plain assignability, so the member collapsed to never.

I reworked it to match on { type: infer TType } and test true extends (TType extends Descriptor ? true : false), which mirrors ExtractEvent's EventDescriptorMatches. A union-typed type field now narrows for both concrete and generic callers, while the original generic (#5448) fix and the invalid-descriptor compile errors stay intact. types.ts is untouched.

Added regression tests for the concrete and generic union-type cases, plus precision and invalid-descriptor guards.

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.

Bug: [5.25.0] typing issue when using assertEvent and generics

1 participant