Diagnosing tRPC Type‑Inference Failures in Nested Routers
When tRPC’s type inference breaks, your router may compile but callers receive wrong or missing types. This guide walks through the symptoms, root causes, step‑by‑step checks, and fixes—plus a concrete example of a nested router that preserves full type safety.
17 Nov 2025, 06:30 UTC

Problem: tRPC Procedure Types Don’t Match Caller Types
In a type‑safe tRPC application, the caller generated by createTRPCProxyClient should reflect the exact input and output types of each procedure. If you see errors such as "Argument of type 'string' is not assignable to parameter of type 'never'" or missing fields in the returned data, the router’s type inference is likely broken.
Recognizable Condition
- Compilation succeeds, but IDE autocomplete shows
neveror missing fields for a procedure. - Runtime errors: the server throws a 500 when a valid request is sent.
- Unit tests compile but fail with type mismatches.
Root Causes & Diagnostic Table
| Cause | Typical Symptom | Why It Happens |
|---|---|---|
| Non‑exported types across module boundaries | Types not visible to the router definition. | TypeScript cannot resolve the type, so it falls back to any or never. |
| Procedures defined in a namespace but not exported | Router compiles but callers see empty signatures. | Namespace boundaries hide types from the inference engine. |
| Middleware that modifies input/output types without proper generics | Caller shows wrong input shape. | Middleware changes the type context but the wrapper type doesn’t propagate. |
| Incorrect router composition syntax | Nested routers lose type context. | Using mergeRouters incorrectly or missing generics. |
Ordered Checks
- Run TypeScript Compiler
Execute
tsc --noEmitin your project root. This will surface any type errors that compile would normally hide. Pay special attention to errors in router or procedure files. - Verify Exported Types
Open the file that declares the procedure’s input/output types. Ensure they are exported with
export typeorexport interfaceand that the file is imported in the router file. - Check Router Import Paths
Confirm that the router imports the procedure modules correctly. Relative paths that miss a leading
./or use an incorrect case can silently fail on case‑sensitive file systems. - Inspect IDE Autocomplete
In your editor, type
router.and observe the inferred type of a procedure. If the signature showsneveror is missing fields, the inference is broken. - Review Middleware Generics
Middleware functions should be typed as
Middlewareor with explicit generics. If you override the input type, make sure you return the correct type. - Validate Nested Router Composition
When merging routers, use
mergeRouterswith the correct generic parameters, e.g.,mergeRouters('user', userRouter). Missing generics can strip type context.
Concrete Example
Below is a minimal but realistic setup that demonstrates proper type inference across a nested router with middleware.
// src/types/user.ts
export type UserInput = { id: string };
export type UserOutput = { name: string; age: number };
// src/procedures/user.ts
import { createProcedure } from '@trpc/server';
import { UserInput, UserOutput } from '../types/user';
export const getUser = createProcedure
.input(UserInput)
.output(UserOutput)
.query(async ({ input }) => {
// Simulate DB fetch
return { name: 'Alice', age: 30 };
});
// src/middleware/auth.ts
import { createMiddleware } from '@trpc/server';
export const authMiddleware = createMiddleware(async ({ ctx, next }) => {
if (!ctx.user) throw new Error('Not authenticated');
return next();
});
// src/routers/userRouter.ts
import { createRouter } from '@trpc/server';
import { getUser } from '../procedures/user';
import { authMiddleware } from '../middleware/auth';
export const userRouter = createRouter()
.middleware(authMiddleware)
.query('get', getUser);
// src/routers/index.ts
import { createRouter } from '@trpc/server';
import { userRouter } from './userRouter';
export const appRouter = createRouter()
.merge('user.', userRouter);
// src/server.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './routers';
const server = createHTTPServer({ router: appRouter, createContext: () => ({ user: { id: '1' } }) });
server.listen(4000, () => console.log('tRPC server listening on 4000'));
In this example:
- All types are exported and imported correctly.
- The middleware is typed generically, preserving the input/output context.
- The router merge uses a dot suffix (
user.) to keep the namespace.
Fixes Tied to Findings
- Missing exports – Add
exportbefore type declarations. - Incorrect import paths – Use absolute or correct relative paths; verify case sensitivity.
- Middleware type mismatch – Provide explicit generics or use
anyplaceholders and cast appropriately. - Router merge without generics – Use
mergeRouterswith the prefix string, e.g.,.merge('user.', userRouter).
Escalation Criteria
- If
tsc --noEmitreports no errors but callers still show wrong types, double‑check your IDE’s TypeScript version and ensure it matches the project’stsconfig.json. - If runtime errors persist after type fixes, enable
debuglogging on the tRPC server to inspect middleware execution order. - For complex monorepos, consider adding a script that runs
tsc -bon each package to catch cross‑package type leaks.
Practical Check
After applying fixes, run:
npm run build
# or
pnpm tsc --noEmit
Then open the generated client file or use the IDE’s autocomplete on appRouter to confirm that getUser now shows the correct input and output shapes. If the types appear as expected, the issue is resolved.
Limitations
Type inference can still fail in edge cases such as circular imports or when using dynamic require statements. In such scenarios, explicitly annotate the procedure types or refactor the module structure.
Conclusion
tRPC’s power lies in its seamless type inference across routers, procedures, and middleware. By following the diagnostic steps above, you can quickly pinpoint where the inference chain breaks and apply precise fixes. Remember to keep your TypeScript compiler in sync with your IDE, and always validate the inferred types in the client layer before deploying.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.