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:
- Keep using
unimport normally for all imports.
- Special-case
defineEventHandler / eventHandler when generating Nitro global declarations.
- Omit their default
const foo: typeof import(...).foo declarations from nitro-imports.d.ts.
- 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.
Problem
When
defineEventHandler/eventHandlerare exposed as global auto-imports through Nitro imports presets, the generatednitro-imports.d.tscurrently declares them like this:In this form, TypeScript loses contextual typing for the handler callback, so
eventbecomesany.However, the same helper remains correctly typed when imported explicitly from
#imports.So the problem does not appear to be in
h3itself, 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, andunimport.h3itself looks correcth3defines proper overloads fordefineHandlerand aliasesdefineEventHandler/eventHandlerto it:h3/src/handler.tsh3/src/_deprecated.tsThose overloads work correctly when the function is imported explicitly.
unimportgenerates the problematic declaration shapeunimportgenerates global declarations via:unimport/src/context.tsunimport/src/utils.tsSpecifically, it emits:
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.tsNitro creates the unimport context and writes the generated global types:
nitro/src/nitro.tsnitro/src/build/types.tsSo 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 thenitro-imports.d.tsoutput.Reproduction
This does not reproduce in a completely plain Nitro setup, because Nitro does not auto-register
defineEventHandlerglobally by default.It reproduces once Nitro is configured with an imports preset that exposes the h3 helpers globally, for example:
Then create a route like:
And use simple type assertions:
Actual result
eventis treated asanywhen coming from the global auto-import declaration.Expected result
eventshould be typed exactly the same as whendefineEventHandleris imported explicitly.Important comparison
This works correctly:
This loses contextual typing:
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.tscurrently contains declarations like:For these specific helpers, replacing them with explicit overloads restores contextual typing:
Proposed direction
I think the cleanest Nitro-side fix would be:
unimportnormally for all imports.defineEventHandler/eventHandlerwhen generating Nitro global declarations.const foo: typeof import(...).foodeclarations fromnitro-imports.d.ts.A good place for that seems to be Nitro’s unimport integration in:
nitro/src/nitro.tsusing a Nitro-specific
unimportaddon (extendImports+declaration) rather than file post-processing.That keeps the workaround:
unimport.Notes on scope
I also checked whether this should instead be fixed in
h3orunimport: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:
nitro/h3,nitro-imports.d.tsuses overloads instead ofconst defineEventHandler: typeof ...,tscno longer reports the route/middleware fixtures as failing due toeventbeingany.