A Week Facing 2,731 Type Errors: The Reality Between Flipping strict On and Off
Enabling TypeScript's strict-family options all at once typically floods the build with thousands of type errors and stalls work. Based on a real project case, this compares each option's migration difficulty and blast radius, and lays out a step-by-step roadmap covering which order is safe to enable, what to fix automatically, and what has to be reviewed by hand.
Published by DevInsight.
The feeling of turning on strict: true in TSConfig for the first time is simple. A project that ran fine until then gets covered in red underlines, and compilation stops. The number 2,731 is not necessarily an extreme case. For a service that has grown over several years, this scale of type errors is fairly ordinary.
The problem isn't the count — it's the questions that follow it. Fixing all of it by hand looks like it would take weeks, but turning it off just puts you back in a loose state. So the realistic choice is "turn it on gradually," and to set the stages, you first need to know how much code each option actually touches.
What the numbers show vs. what you actually have to change
strict is a single switch, but it's really a bundle of five different options. noImplicitAny, strictNullChecks, strictFunctionTypes, noImplicitThis, and strictPropertyInitialization all get turned on together. Staring at the 2,731 errors produced by enabling everything at once makes them indistinguishable, but in reality they fall into three kinds.
First, the ones that inference alone can handle. Second, the ones where any has structurally leaked in through API responses or data from storage. Third, logic written to ignore null outright, which requires touching the code.
These three have completely different fix times. If you don't separate them up front, you can't build a staged plan.
One by one, i.e., options one at a time
The safest starting point is noImplicitAny. Most errors that spill out here are cases where a specific function argument or variable moves around without an explicit type, and the fix is relatively mechanical. Adding a parameter type to an empty function or introducing an interface is usually all it takes.
It helps to set one habit in advance here: instead of leaning on whatever type happens to be inferred, write types explicitly whenever you can. For example, rather than receiving the await result of const data = await fetchData() as any, receive it as an interface. This does more than reduce the error count — it makes the next stages easier.
The biggest hurdle comes next: strictNullChecks.
strictNullChecks is what really decides things
This option can't be judged at a glance by error count. The number might even be small. The issue is not "code with no errors" but that "code that only looked error-free" gets exposed.
For example, chained access like user.address.city all becomes a target. The possibility that user or address is optional doesn't produce an error in JS, but at runtime it blows up as an undefined access. This doesn't end with simply adding type annotations. You have to re-examine the data flow itself: whether null can actually arrive on this path, or whether it should have been filtered out at the call site.
This is where automatic fix tooling shows its limits. TS's auto-fix works in the direction of "clearing away" errors, and clearing an error and fixing it are different things. If a wildcard code style that opens up types is applied, errors stop appearing, but that's far from the safety goal.
So this option has to be handled half-automatically and half-manually. Let automation pick candidates, and have a person fix only as much as they can review of the relevant domain logic. To save time here, you need to reduce dependencies. When types are spread all over the place, you can't keep breaking sync everywhere at once.
The mistake of reversing the order
A common mistake in ordering options is to knock out "the fewest" first rather than "the easiest." A low error count doesn't mean easy. strictPropertyInitialization tends to have few errors, but turning it on first causes problems. This option catches properties not initialized in the constructor, and if classes were already bypassing that with nullish coalescing or accessors, enabling it actually touches more than expected.
The more aggressive order is this: turn on noImplicitAny and part of strict first, then strictNullChecks, and finally the rest.
strictFunctionTypes behaves oddly. The fact that function return types are covariant but parameters are contravariant is a concept unfamiliar to the average TS developer, so this single option breaks function signatures in surprisingly many places. In codebases with a lot of callback-taking patterns, errors pile up here. Manual judgment is often needed, because widening a callback's parameters compiles fine but can violate the contract at runtime. For this option, "leave it and handle it last" is close to the correct answer.
The technical pipeline, and what you must never do
Some things look small by the numbers but hide real obstacles. There is exactly one principle you must never violate in this process: don't run lint while strict: true is on.
Rules like no-unused-vars or @typescript-eslint/no-explicit-any were likely already present before turning on strict, and the errors these mobs fire on code right after strict is enabled are a completely different kind of error. The day you toggle both at once is the day control falls apart.
What you can leverage instead is working alongside // @ts-nocheck. Temporarily dropping that comment on a file or module, enabling strict globally, then gradually removing the comment only from files whose errors decrease is what works best in practice.
The problem is that these comments become meaningless if they pile up. If there are a few @ts-nocheck files left unattended for a long time, they're effectively code outside of strict. So you have to track them. It's worth setting up a script that counts the comments in CI.
Full execution, and measurement
Once you're here, you set the moment of actual rollout. On day one, turn on strictNullChecks and pull the remaining error list. Then group the errors into refactoring abstraction units. Process them ticket by ticket over two days, and when each day's work ends, plot the repository's type-safety graph. Not a curve of the error count declining — look at whether the blast radius of type errors on real deploys is shrinking.
The tooling won't do this measurement for you. Don't trust the number that comes out of the lint command; if you count the type-related bugs that actually show up in release notes after deploy, you'll know whether this work created real value.
If this produces no change in deploys within the first two to three weeks, that project may not be in a state where turning on strict can buy it type safety. In that case, strategies like enforcing strict only on new files, or upfront checks on new code (lint, docgen, CI gate), may be the better choice. Keeping this criterion explicit prevents a failure from ending as just a failure.
One last piece of advice. Just because the errors you met along the way number 2,731 doesn't mean fixing those is the end. That figure is, at most, a signal that "types are flowing somewhere all over the code." After walking the whole roadmap, check again whether the cause that produces that number — the practical channel injecting any — is still there. That channel is precisely the point where the next stage becomes necessary.
Comments
Loading comments.
Good Follow-up Reads
Posts connected to the topic you just read.
The Struggle of One Project's Upgrade from `any` Hell to `strict`
Suddenly enabling `strict` in a loosely-typed TypeScript project makes hundreds of errors pour out overnight. This article is a real migration log about enabling strict-family options one by one amid new feature development and bug fixes, weighing the difficulty and effect of each option to work out an optimal ordering. It covers everything from the `any` cleanup order to `strictNullChecks` and `noImplicitAny`, along with the real-world outages and regressions encountered along the way.
ESLint Flat Config 마이그레이션 실패 일지와 살아남는 체크리스트
ESLint 9의 flat config로 넘어가면서 extends가 사라지고, 플러그인 호환성 문제, VS Code ESLint 확장과의 설정 불일치, 글로벌 변수 선언 방식 변화 등 현장에서 마주치는 장애물을 해결 순서대로 정리한다. 삽질을 줄이는 실전 체크리스트. 2025년 4월, ESLint 9가 정식 릴리스되면서 파일은 deprecated 경고를 넘어 아예 무시되기 시작했다.
The Spots That Quietly Collapse When You Drop eslintrc for Flat Config
When migrating from eslintrc to flat config, what breaks most often isn't the rules but plugin compatibility and the order in which configs are merged. As string-based extends disappears and gives way to arrays of objects, plugin registration, ignores handling, and editor integration all change. This article points out the places that fail silently during real migrations and offers criteria for deciding what to move first and what to throw away.
Previous post
Can Small Services Survive? Limits of Polling and Criteria for Introducing Queues
Next post
One 'use client' Line Splits Server and Client, and Shakes Everything From Bundle Size to State Management
DevInsight Digest
Keep every new article in one calm feed.
Follow the full publication feed without promotional alerts.