In most projects TypeScript is tooling. In Hono it is part of the shipped product: route definitions accumulate a type-level schema, and that schema is what powers hc, the bundled RPC client. That is why the repo treats type-check performance as a CI-guarded metric alongside bundle size.

Types carry the API schema

Hono<E, S, BasePath> threads three type parameters everywhere: E (bindings and context variables), S (the accumulated route schema), and the base path. Every app.get(...) widens S; app.route() merges sub-app schemas (MergeSchemaPath in src/types.ts, a file of ~2,800 lines that exists purely for this machinery and is excluded from coverage because it compiles to nothing).

The payoff is the client in src/client/client.ts: a Proxy that turns property access into URL paths and calls into typed fetches -- no code generation step:

src/client/client.ts
const createProxy = (callback: Callback, path: string[]) => {
  const proxy: unknown = new Proxy(() => {}, {
    get(_obj, key) {
      if (typeof key !== 'string' || key === 'then') {
        return undefined
      }
      return createProxy(callback, [...path, key])
    },
    apply(_1, _2, args) {
      return callback({
        path,
        args,
      })
    },
  })
  return proxy
}

At the type level, Client<typeof app> maps the same route schema onto that proxy, so client.api.user.$get({ query: { id: '1' } }) type-errors when the server route changes. The runtime cost of "RPC" is a proxy and fetch; the contract lives entirely in types.

Type-check time is benchmarked in CI

Because apps instantiate these generics on every route, slow types would be a user-facing regression. The perf-measures/type-check/ harness generates a large synthetic app and measures full diagnostics -- under both the standard tsc and tsgo, the Go-native TypeScript preview (@typescript/native-preview in devDependencies):

.github/actions/perf-measures/action.yml
- name: Performance measurement of type check (tsc)
  run: |
    bun scripts/generate-app.ts
    bun tsc -p tsconfig.build.json --diagnostics | bun scripts/process-results.ts > diagnostics-tsc.json
  # …
- name: Performance measurement of type check (typescript-go)
  run: |
    bun scripts/generate-app.ts
    bun tsgo -p tsconfig.build.json --diagnostics | bun scripts/process-results.ts > diagnostics-tsgo.json

Results are posted to PRs via octocov, so a change that doubles instantiation depth in src/types.ts shows up as a red diff the same way a bundle-size regression would.

Types are tested like code

  • src/types.test.ts and src/client/types.test.ts assert type-level behavior (using expectTypeOf-style checks compiled by the spec tsconfig -- bun run test runs tsc -p tsconfig.spec.json before Vitest precisely to catch type regressions).
  • zod sits in devDependencies solely so validator and client type tests can exercise realistic schema inference; the shipped code never imports it.
  • The hono/factory helper (src/helper/factory/) exists mostly as type plumbing: createMiddleware and createHandlers preserve Env/Input inference for middleware defined outside an app.use() call -- its implementation is a few lines, its value is the overload stack.

For how the compiled output preserves these types across module formats, see Build and release pipeline.

Sources: src/client/client.ts, src/types.ts, .github/actions/perf-measures/action.yml · last synced 2026-08-10 · 26de731 · version 4.13.1