Typescript, losing Zod and tRPC types across monorepo projects, types result in any
Asked Answered
N

3

12

I am in a bit of a weird situation. For the past 2 weeks I've been trying to debug as to why I am losing types between my projects inside a monorepo. My backend exposes the types that my client uses, but for some reason, certain types just don't get across and become any. This has made me unable to develop anything on this project for a while. I made a sample repo out of the issue to further showcase it.

The project is built with Yarn Workspaces and it's structured as following

  • apps/site, the NextJS client importing the tRPC AppRouter
  • apps/backend, the express backend that is exposing the AppRouter
  • apps/config, here are the base tsconfigs used throught the project
  • packages/frontend-shared, not important for this issue, shared UI components

The problem can be found inside the client in the apps/site/src/lib/ApiProvider.ts

// The type is imported directly from backend, here we use type alias to make it cleaner
import type { AppRouter, EmailType, ProfileType, Test } from "@company/backend/trpc";

export type { AppRouter } from "@company/backend/trpc";
import { inferProcedureOutput } from "@trpc/server";

// The type is inferred to any
// Also if you hover over the app router, the context is also any
type loginOutputType = inferProcedureOutput<AppRouter["user"]["login"]>;
//Profile type doesn't have test field but it lets me set it
const a: ProfileType = {};
a.test = false;

//Same as well here, but it errors out as it should
const b: EmailType = {};
b.test = false;

//
const t: Test = {}

The types for tRPC method output are inferred to any for some reason, the const a type is alias to Profile but the type checker doesn't complain even if I add fields that don't exist.

The const b and const t have correct typing

My setup is pretty standard as far as the typescript configuration, I use this base tsconfig which sets some sane defaults like strict and all of the other configs inherit 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 tried tinkering with the tsconfigs, redoing them entirely, tried deleting path aliases, cleaning the yarn cache, tried using project references from frontend to backend but I kept having the same issue

It's very difficult to debug why as there is only typescript magic happening here, no errors or anything of sorts I can look into, I followed the tRPC setup guide but for some reason, some setting or causing types to be broken.

I am 90% sure the issue is not in fact the tsconfig's as I also copied entire setups from other people and it still resulted in the same type inference. I have no idea what else affects typescript in this way, my last resort seems to be to make the API layer into a package and use directly import it inside my packages, but that's hacky and would require quite a bit of refactoring, while I am 100% certain that my current setup should indeed work

Northwest answered 24/10, 2022 at 18:14 Comment(3)
I had the same issue, so I set up minimal working example and started adding code until types resulted in any. Turns out it was because I used infer in one of my custom types.Angelesangelfish
Faced a similar issue. I was using path alias to reference my backend types. The problem was that I was using path alias in the backend as well. Once I was using relative path in the backend the frontend got the types correctly.Expressivity
@Expressivity oh, that might be the issue, I got around it by importing the transpiled code in the /dist directly. It's a bit ugly but it got me out of this 1 month rutNorthwest
S
12

I had a similar issue. I had a monorepo setup, with 2 packages one trpc-backend and mobile-app. To fix this issue on the frontend app I had to add the references property to my frontend tsconfig.json to be

{
 ...
 "references": [{ "path": "../trpc-backend" }] 
}

on doing this, you'll get a ts warning, saying the referenced project, must have composite set to true. to fix this, go over to the backend code and add the composite property under compilerOptions so...

{
...
"compilerOptions": {
    ...
    "composite": true,
 }
}

and you're done

Situs answered 23/3, 2023 at 19:40 Comment(0)
F
3

Just crediting @oae's answer/comment, which solved the issue for me

The problem was that I was using path alias in the backend as well. Once I was using relative path in the backend the frontend got the types correctly

Edit: but I think there is a more elegant solution, which is to directly emit js output files after transforming the aliased imports into relative ones, something which can be achieved with https://github.com/justkey007/tsc-alias, example:

{
  ...
  "scripts": {
    "build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
   ...
Fairish answered 1/2, 2023 at 12:48 Comment(0)
S
1

In reference to don duke's answer

Project references conflict with the noEmit: true flag in tsconfig.json, which is necessary for Webpack, Parcel, Rollup, etc.

Since you're using Zod types, I'm assuming these are declared in a separate package. In that case, deleting the tsconfig.json file (as advised in this Turborepo post) in your types package fixes the typing issue while preserving noEmit: true

Savina answered 8/11, 2023 at 20:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.