When TypeScript Projects Outgrow the Compiler

Shopify began rebuilding its merchant admin UI in 2017, moving away from a Ruby on Rails ERB front-end to a TypeScript, React, and GraphQL stack. TypeScript's static type-checker caught bugs early, and pairing it with VSCode gave developers real-time feedback without leaving the editor. But as more teams shipped features in parallel, the codebase grew enough that the editor tooling began to struggle.

VSCode's TypeScript language server would effectively freeze during startup, taking roughly two to three minutes to become functional. Developers were left dealing with TypeScript's syntax overhead without any of the tooling benefits. The slowdown became a bottleneck for shipping new features.

Diagnosing the Editor Bottleneck

The investigation started by consulting the TypeScript team's own performance documentation. Shopify's engineers ran the compiler's --extendedDiagnostics flag against the single tsconfig.json that covered the entire project and shared the output with Microsoft's TypeScript team. The response: the Admin codebase ranked in the top 1% of project sizes the TypeScript team had encountered.

The recommendation was to break the monolith into smaller projects using project references, a feature introduced in TypeScript 3.0. This would let the editor load only the code needed for a given file rather than the whole codebase.

Before starting the migration, the team needed a way to quantify improvements. VSCode's TSServer logs worked in theory but were impractical: each startup produced around 80,000 lines of log output over roughly two and a half minutes. Expecting every developer to wade through that wasn't realistic. So Shopify built an internal VSCode plugin called TypeTrack to measure and track editor initialization times across the organization.

Starting Migration at the Leaf Nodes

Migrating a codebase of this size to project references couldn't happen in a single commit. The team identified leaf nodes in the project's dependency graph first—the most widely shared parts with minimal coupling to the rest of the code. In the Admin codebase, the packages and tests folders were natural starting points since they were already designed as isolated projects.

Migrating a folder to a project reference requires a project-level tsconfig.json that declares its own dependencies. When TypeScript's compiler runs against these newly created references, expect errors—most commonly, the compiler failing to resolve modules. The fix for each error follows a pattern:

  1. Create a project-level tsconfig.json for the dependency module.
  2. Add a reference to that new project in the consuming parent's tsconfig.json.

Project references carry specific constraints:

  • The include/files compiler options must cover every input file the project reference relies on.
  • Every referenced project must itself declare a references array, even if empty.
  • All local namespace imports must be listed in the paths field of config/typescript/tsconfig.base.json.

Working Up the Dependency Graph

Once the leaf nodes compile cleanly, the migration proceeds level by level up the dependency graph. The general process for each folder:

  1. Add the target folder to config/typescript/project-references.tsconfig.json.
  2. Create a tsconfig.json for that folder.
  3. Run the TypeScript compiler.
  4. Resolve errors by migrating and referencing any outside dependencies the folder requires.
  5. Repeat until the compiler succeeds.

Large projects can surface hundreds or thousands of errors in step four. The team recommends piping errors to a log file and looking for patterns rather than addressing each one individually. Two common situations recur:

  • The folder is too large. Create a root tsconfig.json that references child folders, then migrate those children using the same general steps.
  • The folder contains spaghetti dependencies. If a folder reaches into code it shouldn't, move shared code into the packages folder as isolated packages.

The Payoff: Faster Editor Startup

After migrating the packages and tests folders, editor initialization time in those folders dropped significantly. The reason is straightforward: when VSCode opens a TypeScript file, it asks the TypeScript Server to analyze that file. The server locates the closest tsconfig.json to understand the project's types. Previously, that meant loading the entire codebase. With project references, the server only loads the files the specific project depends on—a much smaller, more focused set.

Sharing the Migration Load

Shopify's Admin codebase was too large for one team to migrate alone. Instead, the team wrote documentation and provided the internal TypeTrack plugin to other Admin section teams, enabling them to improve their own sections' editor performance. The tooling and guidance let each team migrate the parts of the codebase they owned. The project references migration from the React-Admin team at Shopify is also being contributed to open source, which could help other teams working with large TypeScript codebases. The result is a healthier, more modular codebase where the editor stays responsive regardless of how much code is added around it.