Recently, I've found myself stuck in a puzzling predicament. For the last couple of weeks, I've been trying to troubleshoot why the types are getting lost within my projects housed in a monorepo. Even though my backend exposes the necessary types for my client, some of them inexplicably end up as any
. This issue has effectively halted any progress on this project for a significant amount of time. To illustrate the problem further, I created a sample repository that showcases this issue: check it out here.
The structure of the project involves using Yarn Workspaces
, and it is divided into the following components:
apps/site
: NextJS client importing the tRPCAppRouter
apps/backend
: Express backend exposing theAppRouter
apps/config
: Includes the basetsconfig
s used across the projectpackages/frontend-shared
: Not directly related to this issue, contains shared UI components
The problematic code can be found within the client's file located at apps/site/src/lib/ApiProvider.ts
// The type is imported directly from backend, employing a type alias for clarity
import type { AppRouter, EmailType, ProfileType, Test } from "@company/backend/trpc";
export type { AppRouter } from "@company/backend/trpc";
import { inferProcedureOutput } from "@trpc/server";
// The type gets inferred as any
// Hovering over the app router also shows any context
type loginOutputType = inferProcedureOutput<AppRouter["user"]["login"]>;
//Profile type lacks the test field but allows setting it without errors
const a: ProfileType = {};
a.test = false;
//Similarly with this case, where it should error out due to missing fields
const b: EmailType = {};
b.test = false;
//
const t: Test = {}
The types for tRPC
method output end up being inferred as any
for unknown reasons. While const a
is an alias for Profile
, the type checker fails to flag nonexistent fields. On the other hand, both const b
and const t
have correct typings.
My TypeScript configuration follows standard practices, and I use this base tsconfig
as a template, which sets sensible defaults like strict
, with all other configurations inheriting from it
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"preserveWatchOutput": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": false
},
"exclude": ["node_modules"]
}
I've attempted various adjustments such as tweaking the tsconfigs, completely redoing them, deleting path aliases, clearing the yarn cache, experimenting with project references from frontend to backend, yet the issue persists.
Debugging this matter has proven to be quite challenging since there are no specific errors or indicators to investigate, just TypeScript "magic". Although I followed the tRPC
setup guide diligently, something seems to be misconfigured or causing issues with type inference.
I'm fairly confident that the root cause isn't the tsconfig
itself, especially considering I replicated setups from others only to encounter the same type inference woes. Exploring the option of turning the API layer into a standalone package and directly importing it into my packages feels like a hacky solution that would require extensive refactoring, even though my current setup should theoretically function correctly.