Skip to content

feat: add generic parameter for IEventPublisher in EventBus - #1930

Open
lucas-gregoire wants to merge 3 commits into
nestjs:masterfrom
lucas-gregoire:feat/type-checking-event-bus
Open

feat: add generic parameter for IEventPublisher in EventBus#1930
lucas-gregoire wants to merge 3 commits into
nestjs:masterfrom
lucas-gregoire:feat/type-checking-event-bus

Conversation

@lucas-gregoire

@lucas-gregoire lucas-gregoire commented Feb 16, 2025

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Currently, the EventBus.publish and EventBus.publishAll methods always return any, whereas they could infer types from the publish and publishAll methods of the IEventPublisher interface.

What is the new behavior?

This PR introduces a new generic parameter for the EventBus which represents the type of the event publisher (extending IEventPublisher), allowing to infer the return types of the publish and publishAll methods.

If the event publisher does not have a publishAll method—like in the case of DefaultPubSub—then the return type of publishAll is an array of the publish results from the publisher. This clarifies the behavior of the publishAll method.

By default, publish still returns any while publishAll now returns any[] which better matches the returned array.

Does this PR introduce a breaking change?

  • Yes : publishAll now returns any[] instead of any by default. I think this is a "soft" breaking change.
  • No

For this breaking change, here are the situations to consider:

  • Users calling publishAll without using its result: no change.
  • Users using the result of publishAll:
    • With the default event publisher or a custom publisher without a publishAll method:
      • Casting it to an array of something: this cast will still compile.
      • Casting it to something other than an array: this cast will no longer compile. While this situation doesn't make much sense, it is still possible. Users will have to adjust their code.
    • With a custom event publisher that implements a publishAll method:
      • Casting it to the result of publishAll: this cast will still compile but could be replaced by the inference allowed by the new generic parameter.

Personally, I think this is a breaking change with not much impact.

Other information

I also added some missing generics tests on query handlers.

@lucas-gregoire
lucas-gregoire force-pushed the feat/type-checking-event-bus branch from 04605e0 to c41d5b4 Compare February 17, 2025 10:35
@kamilmysliwiec

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

@lucas-gregoire
lucas-gregoire force-pushed the feat/type-checking-event-bus branch 2 times, most recently from 4632dad to 7a0859d Compare April 28, 2026 14:22
Comment thread test/e2e/generics.spec.ts Outdated
Query,
QueryBus,
} from '../../src';
import { expectTypeOf } from 'vitest';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To draw your attention, it seems like vitest ignores this import. Try including a failing assertion (like expectTypeOf(false).toBeNumber();) in this file. Run the test from command line and it will pass.

The feature is marked experimental. It is not working as expected rather often. Here similar findings: ngrx/platform#5148 (comment) or flint-fyi/flint#2398 (comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mrazauskas, you were right, and it was worse: the assertions were never
checked at all. expectTypeOf(false).toBeNumber() passed with 44/44 green.

Digging in, both causes turn out to be documented behaviour rather than bugs:

  • typecheck.enabled
    defaults to false, and nothing else in the pipeline ran tsc over test/,
    since npm run build only covers src.
  • typecheck.include
    defaults to '**/*.{test,spec}-d.?(c|m)[jt]s?(x)', so a *.spec.ts file is
    simply never a type test. The run still printed Type Errors no errors.
  • typecheck.tsconfig
    "tries to find closest tsconfig.json", which here is the root one, whose
    node10 resolution can't read Vitest 4's ESM-only type exports. With
    ignoreSourceErrors: false
    the run then exits 1 on 5 source errors while also printing
    Type Errors no errors.

So in this case it was our configuration rather than the feature misbehaving,
though I agree the reporting makes it very easy to miss, which is your point.

Fixed along the documented path: renamed to generics.spec-d.ts so it matches
the default include, set typecheck: { enabled: true, tsconfig: 'test/tsconfig.json' }, and moduleResolution: "bundler" in the test tsconfig.
And since the guide states that "Vitest
doesn't run these files, they are only statically analyzed by the compiler", I
dropped the try/catch + expect(true).toBeTruthy() scaffolding and the runtime
bus instances with it.

Verified by mutation rather than by a green run: forcing EventBus#publish back
to any in src/ now fails with 2 type errors, as does a wrong expectTypeOf
or a removed @ts-expect-error.

@kamilmysliwiec

Copy link
Copy Markdown
Member

some new merge conflicts popped up after merging your other PR

lucas-gregoire and others added 3 commits July 28, 2026 22:13
The expectTypeOf assertions in generics.spec.ts were never verified. They
are a no-op at runtime, and nothing in the pipeline ran tsc over test/, so
a deliberately wrong assertion like expectTypeOf(false).toBeNumber() passed
with 44/44 green.

Enabling Vitest's typecheck is not sufficient on its own either: its default
include only matches *.{test,spec}-d.ts, so *.spec.ts files are silently
skipped, and it falls back to the root tsconfig, whose "node10" resolution
cannot read the ESM-only type exports of Vitest 4 / Vite 8.

- rename generics.spec.ts to generics.spec-d.ts so it is picked up, and drop
  the try/catch/finally + expect(true).toBeTruthy() scaffolding, which
  asserted nothing at runtime
- enable typecheck in vitest.config.e2e.ts, pointed at test/tsconfig.json
- set moduleResolution "bundler" for the test tsconfig

Verified by mutation: a wrong expectTypeOf, a removed @ts-expect-error, and
a return-type regression on EventBus#publish each fail the e2e run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lucas-gregoire
lucas-gregoire force-pushed the feat/type-checking-event-bus branch from 7a0859d to e27d3ad Compare July 28, 2026 20:13
@lucas-gregoire

Copy link
Copy Markdown
Contributor Author

@kamilmysliwiec conflicts resolved, rebased on master

I also fixed @mrazauskas' review point along the way: the e2e type assertions
were never verified. I confirmed by mutation that a return-type regression
on EventBus#publish fails the run. build, test, test:e2e all pass
locally and on CI.

@mrazauskas

Copy link
Copy Markdown

So in this case it was our configuration rather than the feature misbehaving,
though I agree the reporting makes it very easy to miss, which is your point.

Yes, all is just about configuration.

Although the tool is reporting tests as passing, it does nothing in fact. For me, this looks rather dangerous. Any accidental change in the future will not be caught. As another example, nestjs/graphql#3936 is similarly adding type tests that do not run, but the report is all green again.

@lucas-gregoire

Copy link
Copy Markdown
Contributor Author

Concretely, then. I regressed CommandBus#execute in src/, dropping its return
type from Promise<R> to Promise<any>:

image

npm run test:e2e fails on it in two places, exit 1:

image

With that same regression in place but typecheck removed from the config, the run reports 23 passed and exits 0. That is the "does nothing" state you're pointing at, and it is what this branch did before. Type regressions on those return types are caught now.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants