Skip to content

Auto-imported defineEventHandler / eventHandler lose contextual typing in generated nitro-imports.d.ts #4118

@owl1n

Description

@owl1n

Problem

When defineEventHandler / eventHandler are exposed as global auto-imports through Nitro imports presets, the generated nitro-imports.d.ts currently declares them like this:

const defineEventHandler: typeof import("nitro/h3").defineEventHandler
const eventHandler: typeof import("nitro/h3").eventHandler

In this form, TypeScript loses contextual typing for the handler callback, so event becomes any.

However, the same helper remains correctly typed when imported explicitly from #imports.

So the problem does not appear to be in h3 itself, but in the way Nitro/unimport-generated global declarations are shaped.


Why I think this belongs in Nitro

I checked the stack across nuxt, nitro, h3, and unimport.

h3 itself looks correct

h3 defines proper overloads for defineHandler and aliases defineEventHandler / eventHandler to it:

Those overloads work correctly when the function is imported explicitly.

unimport generates the problematic declaration shape

unimport generates global declarations via:

Specifically, it emits:

const foo: typeof import("...").foo

That shape seems to be the point where contextual typing is lost for overloaded handler aliases like defineEventHandler.

Nitro is the layer that owns the generated nitro-imports.d.ts

Nitro creates the unimport context and writes the generated global types:

So even if the root cause is ultimately related to unimport’s generic d.ts shape, Nitro looks like the most practical place for a targeted fix because it owns the nitro-imports.d.ts output.


Reproduction

This does not reproduce in a completely plain Nitro setup, because Nitro does not auto-register defineEventHandler globally by default.

It reproduces once Nitro is configured with an imports preset that exposes the h3 helpers globally, for example:

imports: {
  presets: [
    {
      from: "nitro/h3",
      imports: ["defineEventHandler", "eventHandler"],
    },
  ],
}

Then create a route like:

export default defineEventHandler((event) => {
  event.context
  return event.context
})

And use simple type assertions:

type Assert<T extends true> = T
type IsAny<T> = 0 extends (1 & T) ? true : false

export default defineEventHandler((event) => {
  type _eventIsTyped = Assert<IsAny<typeof event> extends false ? true : false>
  type _contextIsTyped = Assert<IsAny<typeof event.context> extends false ? true : false>

  return event.context
})

Actual result

event is treated as any when coming from the global auto-import declaration.

Expected result

event should be typed exactly the same as when defineEventHandler is imported explicitly.


Important comparison

This works correctly:

import { defineEventHandler } from "#imports"

export default defineEventHandler((event) => {
  return event.context
})

This loses contextual typing:

export default defineEventHandler((event) => {
  return event.context
})

That difference strongly suggests the issue is not in h3’s exported type, but in the generated global declaration form.


Generated type shape that appears to trigger the problem

Generated nitro-imports.d.ts currently contains declarations like:

declare global {
  const defineEventHandler: typeof import("nitro/h3").defineEventHandler
  const eventHandler: typeof import("nitro/h3").eventHandler
}

For these specific helpers, replacing them with explicit overloads restores contextual typing:

declare global {
  function defineEventHandler<Req extends import("nitro/h3").EventHandlerRequest = import("nitro/h3").EventHandlerRequest, Res = import("nitro/h3").EventHandlerResponse>(handler: import("nitro/h3").EventHandler<Req, Res>): import("nitro/h3").EventHandlerWithFetch<Req, Res>
  function defineEventHandler<Req extends import("nitro/h3").EventHandlerRequest = import("nitro/h3").EventHandlerRequest, Res = import("nitro/h3").EventHandlerResponse>(handler: import("nitro/h3").EventHandlerObject<Req, Res>): import("nitro/h3").EventHandlerWithFetch<Req, Res>

  function eventHandler<Req extends import("nitro/h3").EventHandlerRequest = import("nitro/h3").EventHandlerRequest, Res = import("nitro/h3").EventHandlerResponse>(handler: import("nitro/h3").EventHandler<Req, Res>): import("nitro/h3").EventHandlerWithFetch<Req, Res>
  function eventHandler<Req extends import("nitro/h3").EventHandlerRequest = import("nitro/h3").EventHandlerRequest, Res = import("nitro/h3").EventHandlerResponse>(handler: import("nitro/h3").EventHandlerObject<Req, Res>): import("nitro/h3").EventHandlerWithFetch<Req, Res>
}

Proposed direction

I think the cleanest Nitro-side fix would be:

  1. Keep using unimport normally for all imports.
  2. Special-case defineEventHandler / eventHandler when generating Nitro global declarations.
  3. Omit their default const foo: typeof import(...).foo declarations from nitro-imports.d.ts.
  4. Inject explicit overloads for these two helpers instead.

A good place for that seems to be Nitro’s unimport integration in:

using a Nitro-specific unimport addon (extendImports + declaration) rather than file post-processing.

That keeps the workaround:

  • local to Nitro,
  • limited to these two helpers,
  • and avoids requiring new public API in unimport.

Notes on scope

I also checked whether this should instead be fixed in h3 or unimport:

  • h3: probably not, because explicit import already works and the overloads there look correct.
  • unimport: maybe as a more general future improvement, but a proper generic fix likely needs new API for custom declaration generation and has wider blast radius.

So Nitro seems like the best place for a safe, targeted fix.


So...

I already have a local proof of concept and a regression test that:

  • reproduces the issue with a Nitro imports preset for nitro/h3,
  • verifies that generated nitro-imports.d.ts uses overloads instead of const defineEventHandler: typeof ...,
  • and verifies that tsc no longer reports the route/middleware fixtures as failing due to event being any.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions