h3 icon indicating copy to clipboard operation
h3 copied to clipboard

feat: improve param type inference

Open luxass opened this issue 2 months ago • 5 comments

This PR resolves #1053 by adding automatic type inference for route parameters. When you define a route with parameters using app.get(), app.post(), or any other HTTP method, TypeScript now knows exactly what parameters are available in event.context.params.

Previously, event.context.params was always typed as Record<string, string> | undefined, even when the route pattern clearly defined specific parameters. Now the route pattern is parsed at the type level to extract parameter names and provide full type safety.

Route parameters are now fully typed based on the route pattern you define:

// Before: params were loosely typed
app.get("/user/:id", (event) => {
  const id = event.context.params.id; // string | undefined
});

// After: params are inferred from the route
app.get("/user/:id", (event) => {
  const id = event.context.params.id; // string ✨
});

This works with multiple parameters too:

app.get("/user/:userId/post/:postId", (event) => {
  event.context.params.userId;  // string
  event.context.params.postId;  // string
});

The existing helper functions (getRouterParam and getRouterParams) also benefit from this:

app.get("/hello/:name", (event) => {
  const params = getRouterParams(event); // { name: string }
  const name = getRouterParam(event, "name"); // string
});

Type inference works across all route registration methods like app.get(), app.post(), app.put(), app.delete(), and app.on(). Routes without parameters have params typed as undefined, so you'll know when there are no parameters available.

When you use defineHandler directly (outside of a route), the route pattern isn't available yet, so params remain untyped as Record<string, string> | undefined. You can still manually type them using the routerParams field in EventHandlerRequest if needed. However, when you inline defineHandler with a route, the params are fully typed automatically:

app.get("/hello/:name", defineHandler((event) => {
  event.context.params.name; // string - fully typed!
  
  const params = getRouterParams(event); // { name: string }
  const name = getRouterParam(event, "name"); // string
}));

The implementation leverages InferRouteParams from rou3 (https://github.com/h3js/rou3/pull/168) for route pattern parsing. I have tried to make the types backward compatible, so if you catch something that doesn't work as before, just tell me and i'll fix it 😅

luxass avatar Oct 11 '25 05:10 luxass

This is an awesome start. Wondering if we could pair it with InferRouteParams from rou3 (https://github.com/h3js/rou3/pull/168) for app.[method] somehow

pi0 avatar Oct 11 '25 17:10 pi0

This is an awesome start. Wondering if we could pair it with InferRouteParams from rou3 (h3js/rou3#168) for app.[method] somehow

Yea, i have already started working on it locally. But ran into some beahviour issues which i am trying to figure out first 👍🏻

Will include it in this PR when i am done.

luxass avatar Oct 12 '25 02:10 luxass

@pi0 This PR should be ready for a quick review when you have a moment - happy to adjust anything if needed 😊

I have tried cleaning the overloads from f7c9ece (#1217) up in 42a9512 (#1217), let me know if i should revert that to the multiple inline overloads approach 👍🏻

luxass avatar Oct 12 '25 05:10 luxass

Sorry, it got delayed @luxass, we will try to review soon (also added @danielroe)

pi0 avatar Oct 23 '25 13:10 pi0

Dear @luxass you don't need to rebase PR on all commits. I can take care of rebase before merge 👍🏼

pi0 avatar Oct 28 '25 07:10 pi0