TypeScript 7 in Monorepos: What I Learned Setting Up tsgo with Turborepo, Nx, and pnpm Workspaces

📅 May 15, 2026
TypeScript 7 in Monorepos: What I Learned Setting Up tsgo with Turborepo, Nx, and pnpm Workspaces
👁 ... views

After I published my article on TypeScript 7 rewriting its compiler in Go, the comments section told a story I should have anticipated: the 10x speedup is great, but how does this actually work when you have 20 packages, three tsconfigs, and a CI pipeline that already feels like Jenga?

Ten times. Not nine, not eleven. Ten.

That’s what the Go compiler delivered on a single codebase. But monorepos don’t scale linearly — they scale combinatorially. Every package reference, every cross-package import, every symlinked node_modules becomes a potential failure point. And when TypeScript rewrote its compiler in Go, it didn’t just change the runtime — it changed how it walks the filesystem. That matters more than you’d think.

So I took our actual monorepo — 14 packages, pnpm workspaces, Turborepo for orchestration — and ran the full migration. I also set up parallel test repos with Nx and npm workspaces to compare. Here’s what I learned, what broke, and what I’d do differently.

The Setup: What We’re Working With

Our monorepo structure looks like this:

monorepo/
├── pnpm-workspace.yaml
├── package.json
├── turbo.json
├── packages/
│   ├── typescript-config/     ← shared tsconfigs
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   └── react-library.json
│   ├── shared/               ← UI component library
│   ├── api/                  ← backend (Hono + tRPC)
│   ├── web/                  ← Next.js frontend
│   └── docs/                 ← VitePress docs site
└── apps/
    ├── admin/                ← admin dashboard
    └── mobile/               ← React Native (via Expo)

This is the Turborepo recommended pattern — a @repo/typescript-config package that other packages extend. It’s clean, it’s documented, and it worked perfectly with tsc (the JavaScript compiler). The question was: does it survive tsgo?

Here’s the thing nobody warned me about: tsgo breaks path resolution in pnpm workspaces.

pnpm doesn’t hoist dependencies the way npm does. It uses a content-addressable store and creates symlinks in node_modules/.pnpm/ to avoid phantom dependencies. This is pnpm’s defining feature — it’s why pnpm is faster and more correct than npm or yarn.

But tsgo’s file watcher doesn’t always resolve those symlinks back to their original workspace paths. Instead, it rewrites paths like:

# What the path should be:
packages/shared/components/ChatSettings.tsx

# What tsgo reports:
node_modules/.pnpm/node_modules/@repo/shared/components/ChatSettings.tsx

I hit this in Zed first — everything went red, LSP diagnostics broke, and the editor thought my workspace packages were external dependencies. The same bug was reported by other developers, and it’s not Zed-specific — it’s a tsgo file resolution issue that affects any editor using the Go language server.

The workaround? If you’re seeing this, add resolvePackageJsonImports: false to your tsconfig.json compilerOptions, or temporarily switch your editor back to vtsls (the VS Code TypeScript language server) until the tsgo path resolution is fixed in a future beta. The compiler still works — it’s the editor integration that breaks.

// tsconfig.json — workaround for tsgo symlink resolution
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@repo/shared/*": ["packages/shared/src/*"],
      "@repo/typescript-config": ["packages/typescript-config/*"]
    },
    "moduleResolution": "bundler",
    "resolvePackageJsonImports": false
  }
}

This doesn’t fix the root cause, but it stops the path rewriting. During beta, this is a tax you pay for early adoption.

Turborepo: The Smoothest Migration

Turborepo was the easiest to migrate. Here’s why: Turborepo doesn’t care which TypeScript compiler you use. It orchestrates tasks — tsc --noEmit, tsc --build, biome check — but it doesn’t invoke the compiler directly in a way that depends on the runtime.

The migration was essentially:

# 1. Swap the dependency in your shared config package
cd packages/typescript-config
npm install @typescript/native-preview --save-dev

# 2. Update each package's devDependency
# In each package's package.json:
# - "typescript": "^6.x.x" → "@typescript/native-preview": "^7.0.0-beta"

# 3. Update turbo.json to use tsgo for type-checking
// turbo.json
{
  "$schema": "https://turborepo.dev/schema.json",
  "tasks": {
    "typecheck": {
      "dependsOn": ["^typecheck"],
      "outputs": ["**/*.tsbuildinfo"],
      "inputs": ["$TURBO_DEFAULT$", ".eslintrc*", "tsconfig*.json"]
    },
    "build": {
      "dependsOn": ["^build", "typecheck"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

The key insight: Turborepo’s dependsOn: ["^typecheck"] means each package waits for its dependencies to type-check first. With tsgo’s 10x speedup, this cascade is dramatically faster. On our 14-package repo:

Tasktsc (TS 6)tsgo (TS 7 Beta)Improvement
typecheck (full)47s5.2s9x faster
typecheck (cached)0.3s0.2smarginal
build (full)82s78s5% (bundled by esbuild)
build (changed)31s28s10%

The build time barely moves because Turborepo already delegates transpilation to esbuild/SWC — not tsc. The 9x speedup is almost entirely in type checking. And that’s exactly where it should be.

The catch: project references. Turborepo uses tsc --build with TypeScript project references for incremental compilation. With tsgo, the .tsbuildinfo files are generated correctly, but the incremental cache is slightly less aggressive in the beta — I saw a ~10% penalty on “changed only” builds versus tsc. The Microsoft team has flagged this for a future beta release.

Nx: Good, But You Need to Tweak the Executor

Nx was trickier — not because of tsgo, but because Nx’s @nx/js:tsc executor assumes the Node.js runtime. You can’t just swap the dependency and expect it to work.

The fix is to use a custom executor or run tsgo via nx run-commands:

// nx.json — custom typecheck target
{
  "targetDefaults": {
    "typecheck": {
      "executor": "nx:run-commands",
      "options": {
        "command": "tsgo --noEmit --project {projectRoot}/tsconfig.json",
        "cwd": "{projectRoot}"
      },
      "cache": true,
      "inputs": [
        "{projectRoot}/**/*.ts",
        "{projectRoot}/**/*.tsx",
        "{projectRoot}/tsconfig.json"
      ],
      "outputs": ["{projectRoot}/**/*.tsbuildinfo"]
      }
    }
  }
}

Alternatively, you can keep using @nx/js:tsc but override the tsc binary in your $PATH. I put a shell wrapper at node_modules/.bin/tsc that calls tsgo instead:

#!/bin/bash
exec "$(dirname "$0")/../../@typescript/native-preview/node_modules/.bin/tsgo" "$@"

This is hacky, but it works today without changing every project’s executor configuration.

Nx’s compute caching still works correctly with tsgo — the cache key is based on input hashes, not compiler output. So even though the compiler changed, Nx correctly invalidates and re-caches.

The gotcha: Nx’s code generation (nx g @nx/react:component) generates TypeScript files that assume standard tsc behavior. The generated tsconfig.json includes "declaration": true and "declarationMap": true — which tsgo handles fine, but if you’re using --erasableSyntaxOnly, decorators in generated code will fail. This is a known edge case.

pnpm Workspaces Without Turborepo or Nx

If you’re running a bare pnpm workspace without an orchestration layer, the migration is even simpler — but you lose the incremental build benefits.

# pnpm-workspace.yaml
packages:
  - "packages/*"
  - "apps/*"
// root package.json
{
  "scripts": {
    "typecheck": "pnpm -r --parallel typecheck",
    "build": "pnpm -r --filter='!./docs' build"
  }
}
// each package's package.json
{
  "scripts": {
    "typecheck": "tsgo --noEmit",
    "build": "tsup src/index.ts --format esm,cjs --dts"
  }
}

The --parallel flag runs type checking across all packages simultaneously. With tsc, this was fine — the bottleneck was CPU, not I/O. With tsgo, the type checking is so fast that pnpm -r overhead becomes noticeable. For small repos (under 10 packages), sequential tsgo --build --project tsconfig.root.json is actually faster than parallel pnpm because the project reference incremental cache beats the process spawning overhead.

This is the opposite of what I expected. With tsc, parallel was always faster. With tsgo, the compiler is fast enough that the orchestration layer becomes the bottleneck.

What I Got Wrong

I went into this expecting the compiler to be the hard part. I was wrong. The compiler worked fine — it was the editor integration that broke.

Here are the three things I didn’t anticipate:

1. The symlink bug would affect CI, not just local dev. Our GitHub Actions CI uses zed-like LSP diagnostics in a pre-merge check. The path resolution bug caused false-positive type errors in CI, and I spent two hours debugging a problem that didn’t exist in production code. The fix was to pin the LSP to vtsls instead of tsgo in CI until the beta matures.

2. --erasableSyntaxOnly breaks codegen. If you enable --erasableSyntaxOnly in your root tsconfig (which I recommended in my TS 7 article), generated code from Nx, OpenAPI generators, or GraphQL codegen will fail if they emit decorators or enum declarations. You need a separate tsconfig.build.json that excludes generated directories, or disable erasableSyntaxOnly for packages that use codegen.

3. Path aliases need to sync across three places. With the Go compiler, the tolerance for mismatched paths in tsconfig, bundler config, and test runner config is lower. Under tsc, a stale alias might work by accident. Under tsgo, the stricter module resolution exposes the inconsistency immediately. This is actually a good thing — it catches bugs — but it means your first tsgo --noEmit run will surface every alias mismatch you’ve been ignoring.

The Honest Comparison: Turborepo vs Nx vs Bare pnpm

CriteriaTurborepoNxBare pnpm
tsgo compatibility✅ Works out of the box⚠️ Needs executor tweak✅ Works, no special config
Incremental buildstsc --build + .tsbuildinfo✅ Compute caching⚠️ Sequential only
CI speedup with tsgo8-9x on typecheck6-8x on typecheck5-7x (parallel overhead)
Editor DX issuesSymlink bug (workaround available)Symlink bug + codegen edge caseSymlink bug
Learning curveLow (JSON config)Medium (graph-based)None (just scripts)
Best forJS/TS monorepos, Vercel ecosystemLarge teams, polyglot reposSmall teams, simple setups

Decision Matrix: What Should You Do?

Use Turborepo + tsgo if: Your monorepo is JavaScript/TypeScript-first, you want the simplest migration path, and you value speed over advanced features. Turborepo’s task orchestration + tsgo’s type-checking speed is the lowest-friction combination in 2026.

Use Nx + tsgo if: You have a polyglot repo (Go + Rust + TS, like my hybrid architecture), need Nx’s code generation, or have a large team that benefits from Nx’s affected commands and dependency graph visualization.

Stick with bare pnpm + tsgo if: You have fewer than 10 packages, don’t need orchestration, and want the absolute simplest setup. But consider adding Turborepo once you cross 10 packages — the incremental caching pays for itself.

Stay on tsc for now if: Your monorepo relies heavily on TypeScript’s programmatic API (ts.Program, custom transformers, ts-morph). The API isn’t available in tsgo until TS 7.1. If you run ts-patch, ttypescript, or custom Babel plugins that depend on the TS AST, wait for the API to land.

What Changed After This Migration

Three things:

  1. Our CI type-check dropped from 47 seconds to 5 seconds. That’s not a typo. Our PR safety net (the Pattern 1 CI pipeline) went from feeling like a bottleneck to feeling instant. Developers actually run pnpm typecheck locally before committing now, because it takes 5 seconds instead of 47.

  2. The editor bug forced me to audit our path aliases. We had three stale aliases that worked under tsc by accident. tsgo’s stricter resolution caught them on day one. I consider this a feature, not a bug.

  3. Our team stopped fighting over lint + type-check ordering. With tsgo, type-checking is fast enough to run before every commit. We moved tsgo --noEmit into our pre-commit hook alongside Biome. Previously, we ran type-check only in CI because it was too slow locally.

The Bottom Line

TypeScript 7’s Go compiler works in monorepos — but the beta has a real path resolution bug with pnpm symlinks that breaks editor integration. It doesn’t break the compiler or CI, but it will make your IDE miserable until it’s fixed.

Turborepo is the smoothest migration path. Nx requires an executor tweak. Bare pnpm works but loses incremental build benefits.

The speedup is real, the editor bug is real, and the trade-off is worth it — as long as you understand what you’re getting into.

During beta, the compiler binary is called tsgo — at stable release it reverts to tsc. They’re the same compiler. And when stable ships, this article’s workaround sections will be historical artifacts. But the architectural lessons — how tsgo changes the balance between compiler speed and orchestration overhead — will still matter.

  1. The symlink bug affects editor integration, not compilation — use vtsls as a workaround until the beta fixes it.
  2. Turborepo + tsgo gives 8-9x type-check speedup with zero config changes — the lowest-friction path.
  3. The Go compiler exposes stale path aliases and config mismatches that tsc tolerated — fix them, don’t work around them.

💡

Enjoying the content? Here are tools I personally use and recommend:

  • 🌐 Hosting: Bluehost — what this blog runs on
  • 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use

Purchases through my links help keep this blog ad-free 💙

Enjoyed this post?

Subscribe to the newsletter or follow on YouTube for more dev content.

🎬 Watch Shorts