TikTok for Developers
Modernizing your TypeScript codebase with bulk suppressions
by Chao Guo and Zhuocheng Wu, Software Engineers, TikTok
Tech @ TikTok
Open source

JavaScript's great by itself. But if you want to scale up, to do large projects with large teams, everyone knows you need TypeScript. Amazingly, TypeScript doesn't change your JavaScript code all that much—it just checks for mistakes. And every new release of the compiler brings new notations and new safeguards that promise to make your code even better. But if you have thousands of source files, or thousands of TypeScript projects in TikTok's case, enabling these new checks can be a lot of work. You turn on a setting in tsconfig.json, and suddenly you might be looking at 5,000 build errors. That's too much to fix! What to do?

Exempting old code

When modernizing coding standards, people quickly realize that we care most about new code. We certainly want to enforce best practices for new work, whereas often there isn't reasonable justification to go picking through thousands of old files. We can fix the old code later, when a new feature gives us a reason to work on those files.

The easiest way to exempt old code is to simply ignore errors in entire files. However, when our team researched this, it wasn't as easy as it sounds. The compiler configuration (tsconfig.json) can apply different checks to different file paths, but in practice this is clumsy and requires managing a delicate dependency boundary between old and new file groups. A much more powerful approach is to invoke the compiler once for all files, using a "wrapper" tool that intercepts errors and filters them for old files. We found a project tsc-silent that basically does this, although it's missing some features and seems to be no longer actively developed.

The need for scopes

There's a deeper problem, though: If we exempt an entire file, engineers can continue adding arbitrary amounts of code to that file without following best practices. Even if an engineer is a good citizen, converting one file at a time can be too time consuming. Instead, exemptions should apply to narrower scopes such as a function body within a file.

The TypeScript compiler provides a // @ts-ignore code comment that can suppress a single error, similar to // eslint-ignore-next-line for ESLint. But for a large team, this leads to the "broken windows" effect, where engineers get used to seeing suppressions everywhere in the codebase. Nobody will raise an eyebrow for a merge request that adds suppressions to new code instead of old code.

Some other members of my team already went down this path with ESLint, which produced the eslint-bulk tool. You can read about it in our earlier post A new ESLint feature for large codebases. For today we'll focus on two key principles from that project:

  1. The suppressions are stored in a separate machine-generated file. When enabling new settings for old projects, the eslint-bulk command will do a "bulk" update of the file automatically. Using a separate file prevents "broken windows" in the source code: people know they shouldn't be editing the bulk suppressions file unless they are doing migrations.
  2. A "scopeId" identifies regions within a source file. The eslint-bulk tool introduced an identifier based on the syntax tree. For example, if you want to refer to myMethod() that is a member of MyClass, the scopeId might be .MyClass.myMethod. Unlike approaches that track ranges of line numbers, the scopeId is stable when adding/removing chunks of code or even reordering functions.

Leveraging these ideas, we created a new tool ts-bulk-suppress that applies the same principles for TypeScript bulk suppressions. We've tried to make the two tools as similar as possible, providing a consistent mental model for ESLint and TypeScript migrations.

Give it a try!

Here's a quick look at how to use ts-bulk-suppress with your existing project:

  1. It's recommended to install the tool by running pnpm install ts-bulk-suppress --dev(instead of pnpm, you can substitute npm or yarn according to your package manager preference).
  2. As an example, let's assume that we just enabled noImplicitAny and are now facing thousands of errors. Run this command to automatically generate the .ts-bulk-suppressions.json file:
  3. # Create a bulk.config.json file with suppressions for all compiler errors
    ts-bulk-suppress --gen-bulk-suppress
  4. Make sure the file is tracked by Git:
  5. git add .ts-bulk-suppressions.json
    git commit -m "Enabling bulk suppressions"

    At this point, if you run the tsc command (the official TypeScript compiler), you will still see thousands of errors. But if you instead run ts-bulk-suppress, the suppressed errors will be hidden.


👉 For full documentation, check out our ts-bulk-suppress GitHub page.

Some advanced features

Here's a few other interesting features we included:

  • You can add manual rules to .ts-bulk-suppressions.json. For example, suppose that my-project/src/legacy-sdk contains hundreds of files that we never intend to fix. Rather than tracking thousands of individual suppressions, you could add a manual rule like this:
  • my-project/.ts-bulk-suppressions.json

      . . .
      "patternSuppressors": [
         {
           // Ignore codes TS7006 for all files under that directory:
           "pathRegExp": "/src/legacy-sdk/.*",
           "codes": [7006]
         }
      ]
      . . .
  • Ignoring external errors: The tool provides a way to ignore an error in files anywhere under the node_modules folder, as these files are typically not your team's responsibility. This is similar to skipLibCheck, but scoped to individual errors.
  • Ignoring collateral errors in CI: The compiler's semantic analysis sometimes produces situations where a change in one file creates an error in a seemingly unrelated file. When initially migrating a legacy project, it can be helpful for continuous integration (CI) jobs to ignore files that are not part of the changed fileset.

How it went

Here are some results from our first round of migration:

Total migrated:

51

projects

Teams that used the "patternSuppressors" feature:

4

projects

Teams that enabled "ignoreExternalError":

43

projects

Total TypeScript files in these projects:

18,998

files

Files with at least one bulk suppression:

4,017

files

Files that did NOT require any suppressions:

78.7%


This histogram highlights the top 10 most frequently suppressed errors during this migration:

Count

Code

TypeScript error

10,051

TS2339

Property '{0}' does not exist on type '{1}'.

3,366

TS7006

Parameter '{0}' implicitly has an '{1}' type.

3,234

TS2322

Type '{0}' is not assignable to type '{1}'.

1,840

TS2345

Argument of type '{0}' is not assignable to parameter of type '{1}'.

1,348

TS18046

'{0}' is of type 'unknown'.

1,415

TS7053

Element implicitly has an 'any' type because expression of type '{0}' can't be used to index type '{1}'.

796

TS2304

Cannot find name '{0}'.

719

TS2307

Cannot find module '{0}' or its corresponding type declarations.

677

TS6133

'{0}' is declared but never used

626

TS7031

Binding element '{0}' implicitly has an '{1}' type.

Some of these errors sound nontrivial, but they are problems in the type system; the runtime code was previously working and continues to work without issues. The important result is that 78% of files can now move forward using best practices. The legacy issues can be fixed when (or if) new work is required for those files. Without a way to fix problems incrementally, we would not have been able to adopt the newer compiler configuration at all.

The road ahead

Although ts-bulk-suppress enabled us to get more projects migrated to use our recommended checks, the story isn't over. We're planning more features such as integration with toolchains (that invoke tsc via an API), watch mode, and also support for the VS Code language service. The problems solved by this tool are not unique to TikTok. Anyone trying to modernize a large TypeScript codebase will face these same challenges. If you find this tool useful, maybe you can contribute to its development! Or simply create a GitHub issue and tell us about your experience. That's all for today!

Share this article
Discover more
Stop, Think, Secure: TikTok’s Fight Against ATO Fraud with UK PartnersTikTok joins UK leaders to combat online fraud, promoting two-step verification and launching new tools to fight account takeovers, empowering users to #BeCyberSmart and stay secure.
Security
Community
Modernizing your TypeScript codebase with bulk suppressionsIn this post, TikTok frontend engineer Chao Guo shares about "ts-bulk-suppress," an open source tool that his team created. It enables large codebases to adopt the latest TypeScript features incrementally, rather than all at once.
Tech @ TikTok
Open source
Enhancing Online Advertising with Differential PrivacyTikTok’s AdsBPC offers real-time, privacy-preserving advertising measurement through differential privacy, ensuring accuracy, user trust, and compliance with global regulations.
Privacy
Research
Want to stay in the loop?Subscribe to our mailing list to be the first to know about future blog posts!
By providing your email address and subscribing, you consent to TikTok sending you email notifications whenever a new article is posted on our blogs. You may opt out at any time using the unsubscribe link in each email. Read our full Privacy Policy for more information.
TikTok for Developers