Skip to content

Conversation

@masnormen
Copy link

@masnormen masnormen commented Sep 26, 2025

Problem

honojs/middleware#637

Can not call .use() from OpenAPIHono (zod-openapi) without changing the return type into a general Hono instance

// == logger-middleware.ts
// Adds a variable `logger` to `c.var` in the middleware
type Logger = () => ...
export const loggerMiddleware = createMiddleware<{ Variables: { logger: Logger } }>(...);
// == index.ts
// Initialize an OpenAPIHono instance (zod-openapi middleware)
export const baseApp = new OpenAPIHono();
export const baseAppWithMiddlewares = baseApp
  .use(loggerMiddleware())
  // Because we called .use(), the return type is now
  // `Hono<{ Variables: { logger: Logger } }>` instead of `OpenAPIHono`
// == auth.ts
// ❌ Can not call openapi() because baseAppWithMiddlewares is now a more basic `Hono` instance
const authRoutes = baseAppWithMiddlewares
  .openapi(someRoute, someHandler)

// ❌ `c.var.logger` is not available via TypeScript, although in runtime it should be there
const authRoutes = baseApp
  .openapi(someRoute, (c) => { c.var.logger("...") } ) // error

Current solution

People tried adding custom Env to the OpenAPIHono instance

// == index.ts

// Defining your own Variables
type Variables = {
  logger: Logger
}

export const baseApp = new OpenAPIHono<{ Variables: Variables }>();
export const baseAppWithMiddlewares = baseApp
  .use(loggerMiddleware())
// == auth.ts
// ✅ `c.var.logger` is now available

const authRoutes = baseApp
  .openapi(someRoute, (c) => { c.var.logger("...") } ) // inferred correctly

Others also used approach like extending the interface ContextVariableMap and use type augmentation (like in hono/request-id middleware). But both methods have the same issue:

⚠️ Accidentally changing/removing .use(loggerMiddleware()) in index.ts will risk it being broken on runtime.

Solution

By introducing new attribute ~env in Hono class, we can get the inferred Env types from Hono instance that has been added using .use(middleware)

The naming ~env and ~schema (prefixed with tilde) was choosen to de-prioritize it in autocompletion. By contrast, an underscore-prefixed property would show up before properties/methods with alphanumeric names. As this is mostly just a type-only thing, I also made it return undefined.

// == index.ts

// No need to define your own Variables
// No need to augment ContextVariableMap, just use the real thing

const _baseApp = new OpenAPIHono();
export const baseAppWithMiddlewares = _baseApp
  .use(loggerMiddleware())

type BaseAppEnv = (typeof baseAppWithMiddlewares)['~env']

// THE POINT:
// - Export the original `_baseApp`, but still
// - Retain the inferred end result types from baseAppWithMiddlewares
export const baseApp = _baseApp as OpenAPIHono<BaseAppEnv>
// == auth.ts
// ✅ `c.var.logger` is now available
const authRoutes = baseApp
  .openapi(someRoute, (c) => { c.var.logger("...") } ) // inferred correctly

I'm not sure what kind of tests to add here, as it only forwards the generics E and S in Hono class, and contains no runtime code.

To complement the expose of ~env, I also added ~schema, so people can get both generics. Seems that exposing schema would make type inference too deep.

@masnormen masnormen changed the title feat(hono-base): introduce ~env and ~schema type-only attributes in Hono class feat(hono-base): introduce ~env type-only attribute in Hono class Sep 27, 2025
@masnormen
Copy link
Author

Previously I also exposed ~schema for the Schema generics, but seems that it made the type inference too deep. Just removed it

@codecov
Copy link

codecov bot commented Sep 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.26%. Comparing base (81bda2e) to head (d24f332).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4428   +/-   ##
=======================================
  Coverage   91.25%   91.26%           
=======================================
  Files         171      171           
  Lines       10915    10916    +1     
  Branches     3147     3146    -1     
=======================================
+ Hits         9961     9962    +1     
  Misses        953      953           
  Partials        1        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yusukebe
Copy link
Member

yusukebe commented Oct 3, 2025

Using ~env is an interesting idea, but I think we don't need it. It's okay to make just a utility type like this:

type ExtractEnv<T> = T extends Hono<infer Env, any, any> ? Env : never

Usage:

import { Hono } from 'hono'

const app = new Hono<{
  Variables: {
    foo: string
  }
}>()

type Env = ExtractEnv<typeof app>

/* Env is:
type Env = {
    Variables: {
        foo: string;
    };
}
*/

We must carefully consider adding property to hono-base.ts, as the change is slight and affects the file size of the application. It's good if it can be done with only a utility.

@masnormen
Copy link
Author

type Env = ExtractEnv

You're right. I forgot that you can infer generics. Will revise this PR with just introducing a utility type then.

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.

2 participants