Migrate to TypeScript 7 Without Breaking Your Monorepo

By Diogo Hudson Dias
Senior engineer in a São Paulo office examining a tsconfig file and CI dashboard on dual monitors during the evening.

Major TypeScript upgrades don’t break your app in production. They break your CI, your editor, and your schedule. TypeScript 7 will be no different. The type‑checker tightens, old escape hatches close, and your monorepo lights up red because union narrowing got smarter in one corner and your decorators plugin lagged a week in another. If you treat this like a Friday dependency bump, you will lose a sprint.

This isn’t a rant against upgrades. It’s a playbook for migrating to TypeScript 7 without stalling delivery. The goal: ship features while flipping the compiler. That means two pipelines (build vs. types), staged flags, a baseline you can trend to zero, and a small, accountable tiger team. If you lead engineering at a US startup running a 300K–2M LOC TypeScript estate, read on.

Why this upgrade matters to a CTO

  • Compiler semantics shift in majors. Even when the runtime output is identical, a change in control‑flow analysis or JSX typing can invalidate thousands of lines. Your red CI isn’t a bug; it’s your new reality.
  • Your ecosystem moves with you. Frameworks, linters, and type packages gate support by major. If your product depends on Next.js, Vite, or a design system that pins tooling, you will eventually be forced to move.
  • The cost is real but bounded. In practice, we see 0.5–1.5 FTE‑weeks per 100K LOC to migrate a healthy codebase with a mature pipeline. On a 1M LOC monorepo, that’s 1–3 focused weeks for a small platform pod.

What changes break teams (regardless of version specifics)

  • Module resolution drift. ESM/CJS dual packages, "exports" maps, and different moduleResolution modes de‑stabilize path aliases and deep imports. Your Jest/Vitest config often lags behind your build config.
  • Stricter default flags or narrowed inference. A smarter checker reveals unsafe patterns you relied on. 70–80% of errors usually bucket into a handful of anti‑patterns.
  • Decorators and metadata. If you use ORMs or frameworks with decorators, you’re hostage to their release cadence. One minor lag can block a major compiler upgrade.
  • Build vs. types coupling. Babel/esbuild/swc transpile fine while tsc fails, or vice versa. Teams who tied emit to type‑checking have to pick one to unblock the other.

The meta‑strategy: two pipelines, one baseline

Stop mixing compilation with type‑checking. Your runtime pipeline should not depend on the checker’s mood today.

  1. Split build and types. Use esbuild or swc for transpile/bundle. Run tsc --noEmit for type‑checking. Keep them in separate CI jobs with independent caching and owners.
  2. Add a TypeScript "duet" stage. For a few weeks, run TypeScript 6 (current) and TypeScript 7 (upgrade) concurrently in CI. TS6 remains gating. TS7 reports to a baseline file that must trend down daily.
  3. Establish a baseline. First TS7 run: capture all errors into a baseline. Do not waive everything globally. Create targeted, reviewed suppressions in code with a clear expiry date.

Pre‑migration hygiene (one sprint of boring work that pays for itself)

1) Inventory your surface area

  • Size and shape: Count LOC by package. Identify top 20 packages by compile time and error density once you flip a trial build. You need a map before you choose battles.
  • Type holes: Measure any usage rate. If more than 10–15% of files rely on implicit any or aggressive as any, set expectations: migration will surface a lot of red.
  • Test runner footprint: How many Jest configs? Any legacy ts-jest? If yes, plan to move to ts-jest with isolated transpile or switch to Vitest with esbuild/swc to avoid type coupling.

2) Untangle module resolution

  • Standardize on NodeNext semantics. Mixed ESM/CJS packages are a fact. Align tsconfig, Node runtime, bundler, and test runner on the same module story. Do not ship a different module graph to prod than you test locally.
  • Kill deep import habits. Ban imports like lib/something/internal across packages. Use public entry points. Add a linter check to catch deep imports.
  • Make path aliases honest. If tsconfig.paths maps @app/foo to src/foo, your bundler and test runner must resolve it identically. Drift here creates spooky action at a distance in upgrades.

3) Embrace project references

If you run a monorepo and you are not using TypeScript project references, you are leaving 20–40% compile time on the table and you have fewer levers during upgrades. Composite builds give you per‑package boundaries and cacheability that make red/green cycles survivable.

  • Make packages composite. Add composite: true and boundaries. Generate .d.ts for leaf packages that export types.
  • Use tsc -b locally and in CI. The build mode catches cycles and controls invalidation. On a 1M LOC repo, we routinely see cold build go from 15–20 minutes to 6–10 with references and caching.

4) Stage stricter flags before the jump

You can reduce surprise by enabling stricter checks under today’s compiler. Pick 2–3 flags your codebase can tolerate and ramp them with waivers:

  • noUncheckedIndexedAccess to surface unsafe array/object access.
  • exactOptionalPropertyTypes to stop conflating “missing” with “undefined”.
  • useUnknownInCatchVariables to end lazy any in error handlers.

Turn them on in a handful of packages with a clear per‑team quota of fixes per week. The payoff is lower blast radius when you flip to TS7.

The migration, step by step

Phase 0 (1–2 days): Freeze and fork

  • Lock the world. Freeze your toolchain: Node version, package manager, bundler, test runner, and type packages. Upgrading the compiler while also jumping Node or Jest is how teams create week‑long regressions.
  • Fork the compiler. Add TypeScript 7 as a separate dev dependency in a parallel CI job. Keep TypeScript 6 as the gating checker for main.
  • Create the TS7 error baseline. First full run of tsc --noEmit with TS7 generates your “debt”. Store it. Publish it daily. Your platform pod owns getting it to zero.

Phase 1 (3–5 days): Burn down the 80%

  • Bucket errors. In a 500K–1M LOC repo, expect 1,000–5,000 initial errors. 70–80% are repetitive patterns. Examples we see every time: missing satisfies guards for object literals, brittle conditional types exposed by stricter narrowing, and path alias leakage to runtime.
  • Automate the boring fixes. Use ts-morph or codemods to apply the same local refactor across dozens of packages. Add lint rules so the anti‑pattern doesn’t grow back.
  • Publish the burn‑down. Treat this like paying down incidents: daily chart to the org. You want a straight line to zero by end of week one.

Phase 2 (2–4 days): The stubborn 20%

  • Decorators and ecosystem lag. If you rely on a framework’s decorator or reflection plugins, coordinate with that team or vendor. Pin to a known‑good version. If support is pending, isolate those packages behind a temporary boundary and waive only there.
  • Public API surfaces. Libraries that publish .d.ts need special attention. Run API Extractor to verify public API is stable after the compiler switch.
  • Test runner parity. Ensure Jest/Vitest resolve ESM/CJS the same way your bundler and Node do. Mismatches here cause “works in dev, fails in CI” failures that look like TypeScript regressions but are really module bugs.

Phase 3 (1 day): Flip the gate

  • Promote TS7 to gating. When baseline hits zero and the main branch has been green for 48 hours with the duet job, make TS7 the gating checker. Keep the TS6 job for a week as a canary, then remove it.
  • Unfreeze. Unpin the ecosystem carefully. Upgrade one class of tool per day (linters, test runners, then bundlers) to avoid combinatorial explosions.

Performance and reliability guardrails

  • Cache everything. Remote cache your esbuild/swc output and tsc -b artifacts. On a 1M LOC repo, CI time drops 30–50% with reliable cache keys.
  • Keep skipLibCheck on for CI unless you publish libraries. Then add a separate, nightly “libcheck” job to catch type package drift without taxing every PR.
  • Measure the delta. Track build time and bundle size before/after. For a healthy repo, expect tsc --noEmit wall‑time within ±10% and no meaningful runtime change. If you see larger regressions, it’s usually a new type‑level recursion explosion in a single package—find it and simplify.

People and cost model

You do not need a swarm. You need a small pod that can make unglamorous changes quickly and coordinate calmly.

  • Team: 2–4 senior engineers from platform/tooling with authority to touch all packages. Add one representative from your largest product team for context.
  • Time: 1–3 weeks for 1M LOC. Healthy monorepo with project references and modern test/build stack leans closer to one week. If you are missing references and still transpile via tsc, budget three.
  • Overlap matters. If you use a nearshore pod in Brazil, you’ll get 6–8 hours of US overlap for the daily burn‑down and dependency wrangling, and 20–30% lower cost for the monotony of codemods and config diffing.

What will break (and how to defuse it)

  • Jest with ts-jest in ESM repos. If you’re still compiling types through Jest, move to native ESM with Babel/esbuild for transform and leave types to tsc --noEmit. Or adopt Vitest where possible.
  • Path aliases at runtime. Imports like @app/foo work in editor but explode in Node. Resolve them in your bundler/test and build real relative imports in emitted code. A simple pre‑flight “no unresolved aliases” check saves hours.
  • Barrel files and circular deps. The checker gets better at detecting subtle cycles. Run dependency-cruiser and break cycles explicitly before you flip.
  • Ambient type leaks from tests. Global declarations bleed into source in monorepos with sloppy globs. Scope your types field in tsconfig and split test types from prod types.

A real result pattern (sanitized)

At a fintech scale‑up with ~700K LOC TypeScript across 86 packages, we ran a two‑week TS major upgrade with zero feature freeze:

  • Week 0: Added project references to 18 lagging packages; moved tests off ts‑node; standardized module resolution.
  • Week 1: Dual checker in CI (old gating, new baseline). 2,840 initial TS7 errors; 2,250 fixed with three codemods and two lints. CI time dropped from 28 to 17 minutes after references and remote caching.
  • Week 2: Decorator‑heavy ORM package blocked for 3 days pending vendor patch; isolated and waived locally. Baseline to zero; promoted TS7 to gating. No production incidents. Developer sentiment: fewer phantom IntelliSense errors.

When you should not migrate yet

  • Your core framework isn’t ready. If your app depends on a framework that hasn’t declared support, wait. Do not hard‑fork your own decorators plugin unless you’re willing to own it.
  • You’re also changing Node, bundler, or test runner. Split the changes. One dimension at a time. The reliability math is not on your side otherwise.
  • Your type debt is catastrophic. If implicit any is everywhere and your tests depend on ts‑node, do a one‑sprint hardening first. You’ll save two in the upgrade.

Governance: keep the gains

  • Add a quarterly compiler bump ritual. Put it on the platform roadmap. Small steps beat multi‑year leaps.
  • Keep the duet job around. You don’t need two majors forever, but rerun the duet pattern for each minor/major. It’s cheap insurance.
  • Track type health as a KPI. Any rate, number of suppressions, and per‑package error density are leading indicators of future upgrade pain.

If you want help

Our Brazil‑based nearshore pods do this work all year: untangling module resolution, rolling project references across monorepos, codemodding away brittle patterns, and keeping CI green while you ship. You get US overlap (6–8 hours), hardened checklists, and a team that has broken (and fixed) these things before.

Key Takeaways

  • Split build from types; run esbuild/swc for emit and tsc --noEmit for checking.
  • Run a dual‑checker CI (current as gate, TS7 as baseline) for 1–2 weeks.
  • Adopt project references and remote caching to tame compile times.
  • Standardize on one module story (NodeNext) across runtime, bundler, and tests.
  • Fix the repetitive 70–80% of errors with codemods; isolate the stubborn 20%.
  • Budget 0.5–1.5 FTE‑weeks per 100K LOC; use a small platform pod, not a herd.
  • Don’t upgrade alongside major Node/bundler/test changes; one axis at a time.
  • Treat type health as a product metric so the next upgrade is boring.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation