Vicedomini Softworks

Software Development

TypeScript on Large Projects: Structure, Config, Migration

22 October 2026

Engineer migrating a large software codebase

TypeScript pays off most on large projects, where many files, many contributors and long maintenance cycles turn implicit assumptions into defects. The benefit is not automatic, though. A large codebase needs a strict compiler configuration, a structure that keeps type-checking fast, and a migration plan that never freezes delivery. This article sets out how each of the three is handled in practice, with measured results from teams that have done it.

Vicedomini Softworks
vicedominisoftworks.com
Build TypeScript for the Long Term
Vicedomini Softworks engineers custom software, including web applications and backend systems, with an emphasis on maintainability and long-term support.
Ask for a consultation

TL;DR

  • TypeScript is most valuable where codebases are big and long-lived: the type system acts as living documentation and makes refactoring safe.
  • Enable strict mode from the start of a new project, add noUncheckedIndexedAccess, and treat any as a deliberate exception, not a default.
  • Split the codebase into project references with composite and incremental enabled, so the editor and the build only process what has changed.
  • Migrate existing JavaScript incrementally, starting with data models, the API layer and shared utilities, never with a big-bang rewrite.
  • TypeScript 7, the native port, reports build times 8x to 12x shorter on large projects, which changes the economics of very large repositories.

Table of Contents

Why scale changes the TypeScript conversation

On a small application, TypeScript is a convenience. On a large one, it is infrastructure. Educational material on the subject makes the same point: the language gives its best results in projects with many files, several contributors and long maintenance cycles, because the type system works as documentation that cannot drift out of date the way a wiki page does.

The reasons are structural. In a codebase of hundreds of modules, no single engineer holds the whole of it in their head. Function signatures, API payloads and domain models become the contract between people who may never speak to one another. When a contract is written in types, the compiler enforces it on every change; when it lives in comments or in the memory of a senior colleague, it is enforced only by accident.

There is also a cost side that must be stated honestly. Types add syntax, a build step, and a configuration surface that can grow unmanageable. On large projects the real risks are rarely the language itself. They are slow type-checking, inconsistent strictness between packages, and types that are technically present but meaningless because of widespread use of any. The sections below address each of these.

What teams report after adopting it

The evidence from production teams is consistent in direction, even if each figure depends on context and should be read as one team’s experience, not a guarantee.

An engineering team at i3k reported that production bugs tied to types and null or undefined values fell from 12 to fewer than 2 a month, and that code review time dropped by 30%, since the compiler removes trivial errors before a human reviewer sees them. The same team documents onboarding of new developers shrinking from about three weeks to about ten days in some cases, because types communicate intent between team members.

Refactoring is where the effect is most visible. Renaming a critical function in untyped JavaScript is a matter of searching the text and hoping nothing was missed. With types, the compiler lists every call site that breaks, and the change becomes a mechanical operation instead of a gamble.

Scale of adoption matters too. Bloomberg’s engineering group has described how it approached TypeScript across a very large JavaScript estate, noting that the Bloomberg terminal contains more than 10,000 applications, and that the scale of the existing code dictated a deliberate, staged process rather than a single switch.

What teams report after adopting it — overview diagram

Compiler configuration that holds up at scale

The tsconfig.json of a large project is a policy document. It decides how much of the codebase the compiler is allowed to trust, and every relaxation compounds over years.

Strictness as the baseline

Start with strict: true. It enables a family of checks, among them strict null checking and stricter function types, which remove the most common categories of runtime failure. Retrofitting strictness later is far more expensive than starting with it, because every unchecked assumption has by then been built upon.

Two practices deserve emphasis. The first is avoiding the indiscriminate use of any, which switches off type safety for whatever it touches and spreads through inference to code that never mentioned it. Where a type is genuinely unknown, unknown forces the consumer to narrow it before use. The second is enabling noUncheckedIndexedAccess, documented in the TSConfig reference, which makes reads from arrays and index signatures include undefined in their type. It forces explicit handling of missing values in collections, which is one of the most frequent sources of crashes in large applications.

The official handbook also lists avoidable mistakes in type declarations. Among them, it advises against the boxed types Number, String, Boolean, Symbol and Object, which refer to non-primitive objects almost never used appropriately in JavaScript; the lower-case primitives are the correct choice.

A shared base configuration

In a repository with many packages, strictness should not be negotiated package by package. A single base configuration, extended by each package, keeps the rules uniform and makes any deviation visible in code review. Exceptions are then explicit, written down and reviewed, instead of accumulating silently.

Setting Purpose in a large codebase Where to set it
strict Enables the core family of strict checks Base config, all packages
noUncheckedIndexedAccess Forces handling of undefined when indexing collections Base config, all packages
composite Makes a package a referenceable sub-project Each package
incremental Saves compilation information between runs Each package
references Declares dependencies between sub-projects Each package or a solution config

Types that describe states, not just shapes

Beyond compiler flags, the pattern that pays most on large systems is modelling states explicitly. The i3k team describes discriminated unions for application states: a job in an AI pipeline, for instance, is pending, running, completed or failed, and each state carries only the fields that make sense for it. An impossible combination, such as a completed job with no result, simply cannot be written. The same idea applies to payment flows, order lifecycles and permission models in any enterprise system.

Structuring the codebase for fast feedback

As a repository grows, the compiler becomes the bottleneck. The symptoms are familiar: an editor that takes long to show errors after opening a file, a continuous integration job that spends minutes on type-checking alone, and developers who stop trusting the tooling.

Project references

Project references let you split one large program into smaller ones that depend on each other explicitly. The Nx documentation summarises the mechanics: add the references property in each tsconfig.json, enable composite: true, and build with tsc --build. Each package then acts as its own TypeScript program while keeping its relationships to the others.

Shopify’s engineering team documented the effect when it moved its merchant admin codebase, a TypeScript, React and GraphQL application, to this model. In their write-up on migrating to project references, analysis time for individual utility and test files in the editor fell from 155 seconds to roughly 8 to 13 seconds. The result was not a marginal improvement; it decided whether the editor was usable at all.

How many sub-projects

Granularity is a judgement call. Too few sub-projects and each one remains slow; too many and the overhead of managing references and build ordering grows. The range commonly recommended for a monorepo is between 5 and 20 projects, in line with the guidance published alongside the TypeScript 7 announcement. Boundaries should follow the architecture: shared domain models, API clients, UI components and each deployable application are natural candidates.

The official performance guidance on the TypeScript wiki recommends adopting these practices as early as possible, since later changes to project layout are disruptive. It also explains how to investigate slow compilations once the obvious measures are in place.

Boundaries are an architectural matter

Project references only help if the dependency graph is clean. Circular dependencies between packages are rejected by the build, which is a feature: it exposes coupling that would otherwise stay hidden. Teams that find the split painful are often discovering that their module boundaries never really existed. In that sense the exercise is architectural before it is technical, and it is worth doing together with a review of domain boundaries.

TypeScript 7 and the cost of large builds

On 8 July 2026 the TypeScript team announced TypeScript 7, a native port of the compiler written in Go. Benchmarks published with the release show build times for large codebases, including the VS Code and Playwright repositories, between 8x and 12x shorter than with the previous version, with memory reductions of up to 26% on large codebases.

The port parallelises parsing, type-checking and emit, and exposes flags such as --checkers and --builders to control how that parallelism is used. For a large organisation the consequence is practical. Type-checking in continuous integration stops being the slowest step, feedback in the editor becomes near instant, and a single repository can grow further before it has to be divided purely for performance reasons.

Two cautions apply. First, a major compiler version can change behaviour, so teams should read the release notes and run the new compiler in a branch or a parallel CI job before switching. Second, faster tooling does not replace good structure: project references and incremental builds remain sound practice, and a clean dependency graph is still what keeps a codebase understandable.

Migrating an existing JavaScript codebase

Most large TypeScript projects did not begin as TypeScript. The migration strategy that works is incremental. A big-bang conversion blocks feature work, produces enormous pull requests that nobody can review properly, and often ends with any sprinkled everywhere to make the compiler quiet. Shopify’s documented approach and the Bloomberg experience both point the same way.

A workable order, in prose rather than a checklist, runs as follows. Begin by allowing JavaScript alongside TypeScript, so that converted and unconverted files coexist. Convert first the files with the highest leverage: data models, the API layer and shared utilities, because their types propagate to everything that consumes them. Introduce strictness gradually, enabling individual strict flags package by package instead of demanding full strictness from day one. Then use project references to isolate the converted areas as independently compilable units, which also delivers the editor speed-ups described above. Finally, once most of the code is typed, raise the base configuration to full strictness and make new code subject to it by default.

Stages of an incremental TypeScript migration

Two habits keep the migration honest. Track the count of any and of suppression comments over time, so progress is measurable rather than impressionistic. And make every pull request that touches a legacy file leave it slightly better typed than it was found, which spreads the work across the team without a dedicated freeze.

Conventions that keep types useful

A typed codebase can still be a confusing one. Over many years and many contributors, a few conventions determine whether types remain an asset.

Keep types close to the boundaries. Everything entering the system from outside, such as HTTP responses, message payloads and configuration, is untrusted regardless of what the type says; validate it at runtime and derive the static type from the validator, so the two cannot diverge. This matters particularly in event-driven architectures, where message types are shared between producers and consumers. Material on enterprise TypeScript practice describes the same pattern for REST and GraphQL APIs with typed data transfer objects and for Kafka or RabbitMQ messages with shared types.

Prefer narrow, explicit public interfaces for each package, and keep internal types unexported. A package that exports everything invites consumers to depend on details, and those details then become impossible to change.

Treat type complexity as technical debt. Elaborate conditional and mapped types can be elegant, but when only one engineer can read them they become a liability. A good test is whether a new team member can understand an error message produced by the type without help.

Finally, automate enforcement. Linting rules against any, type-checking as a required status check, and a base configuration that cannot be weakened without review turn good intentions into properties of the system.

Our perspective

In our work on enterprise web applications, SaaS platforms and integrations, we rarely see TypeScript fail because of the language. We see it fail because the decision was taken as a tooling choice and not as an architectural one. A team adopts it, leaves strictness off to move faster, and two years later has a codebase with type annotations and none of the guarantees.

We treat the type system as part of the design. That means agreeing the module boundaries and the data contracts first, writing them as types, and letting the compiler hold everyone to them. It also means that the engineers who design the architecture are the ones who stay to build and maintain it, which is how our teams work with clients: directly, without an account-management layer between the people deciding and the people implementing. A technical decision such as the granularity of project references or the strictness policy has business consequences in delivery speed and maintenance cost, and those are best weighed by the same people.

Our recommendation is consistent. For anything that will be maintained by more than a handful of people for more than a couple of years, start strict, split early along real domain boundaries, and migrate legacy code in small, reviewed steps. Where a codebase is already large and in trouble, a code audit that measures any density, build times and dependency cycles will tell you more in a few days than months of opinion.

— Pepe F.

When an outside review makes sense

Some situations justify bringing in senior engineering judgement: a monorepo whose type-check time keeps growing, a stalled JavaScript migration, a choice of stack for a new platform, or a system whose types no longer reflect what runs in production. We cover this through architecture, code audit and custom development services, and for teams that need continuing technical leadership without a full-time hire, our CTO Advisory, Fractional CTO and CTO Partner plans start from €1,800, €4,000 and €7,500 per month respectively, with an initial assessment from €3,500 one-off. Every consulting engagement ends with written recommendations.

FAQ

Is TypeScript worth it for a large project?

For projects with many files, many contributors and a long maintenance horizon, generally yes. The type system documents intent, catches a class of defects at compile time and makes refactoring safer. Teams have reported fewer type-related production bugs and shorter code reviews after adopting it, although the size of the gain depends on how strictly the configuration is applied.

Which tsconfig settings matter most for large codebases?

Start with strict: true and add noUncheckedIndexedAccess, which forces explicit handling of undefined when reading from collections. For speed, use composite and incremental with project references. Keep these in one shared base configuration so every package follows the same policy.

How do I migrate a large JavaScript codebase to TypeScript?

Migrate incrementally, not in one rewrite. Convert data models, the API layer and shared utilities first, enable strict flags gradually, and use project references to isolate converted parts. Track the number of any usages so progress can be measured.

Does TypeScript 7 change how large projects should be organised?

It makes builds and editor feedback considerably faster, with reported build-time reductions of 8x to 12x on large projects. It does not remove the value of a clean dependency graph and project references. Test the new compiler in a separate branch before adopting it across the organisation.

Sources

This article was produced with AI assistance and reviewed for accuracy. It is provided for general information only and is not professional advice.