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:
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):
- 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.tsandsrc/client/types.test.tsassert type-level behavior (usingexpectTypeOf-style checks compiled by the spec tsconfig --bun run testrunstsc -p tsconfig.spec.jsonbefore Vitest precisely to catch type regressions).zodsits in devDependencies solely so validator and client type tests can exercise realistic schema inference; the shipped code never imports it.- The
hono/factoryhelper (src/helper/factory/) exists mostly as type plumbing:createMiddlewareandcreateHandlerspreserveEnv/Inputinference for middleware defined outside anapp.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.