DevInsight

A developer's field notes

Tools
5 viewsAbout 5 min 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.

Published by DevInsight.

#TypeScript#strict#strictNullChecks#noImplicitAny#any#리팩토링#레거시코드#타입안전성#tsconfig#점진적개선

Adding the single line "strict": true and running tsc --noEmit takes only a few seconds. The real problem comes after. In a project that was born loosely typed, errors pour in by the hundreds. I gave up after counting them, and I made that choice twice. It was only after repeatedly enabling and disabling it that I saw a different path.

strict is not a single switch

Strict looks like a single switch, but it's really a collection of options. strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, strictBindCallApply, noImplicitThis, useUnknownInCatchVariables. Turning all seven on at once piles up errors of very different difficulties under one roof. Some only need one line of types fixed; others won't pass until you change a function's behavior. You spend all your time just picking the two apart while looking at an error list. That's why you lose when you enable them all at once.

The story changes once you weigh difficulty and effect per option and settle on an order.

  • noImplicitAny — errors map to exact lines, and the fixes almost never change runtime behavior. Great for beginners, and the effect is large.
  • strictFunctionTypes, strictBindCallApply, noImplicitThis — little code to fix once enabled. Options you can finish within a day.
  • strictNullChecks — by far the hardest. Most of your time gets spent here.
  • strictPropertyInitialization, noUncheckedIndexedAccess — optional. For the latter, skipping it is often the more reasonable choice.

It's better to knock out the easy options first. strictBindCallApply checks the arguments to bind and call, and enabling it leaves only a few spots to fix. noImplicitThis prevents this from staying any in callbacks, and if your codebase is functional-style without this, you'll barely get any errors. useUnknownInCatchVariables turns the e in catch into unknown. Any code that read e.message directly falls apart here, so building a small helper to extract error messages cuts down on the fixes.

Why clean up any first

The reason to hunt down any first runs against intuition. An any type lets null through even with strictNullChecks enabled. If you turn on strictNullChecks while the code is covered in any, fewer errors surface — and fewer errors means exactly that much more real problems being masked. The instinct that fewer errors is better breaks down right here. That's why the any cleanup comes before strictNullChecks.

The any cleanup isn't done file by file. You establish types starting at the boundary where data comes in: API responses, DB rows, and return values from third-party libraries. If you first define the return type of a fetch function that used to receive its response JSON as any, the inner code that calls it can be fixed by following the type that flows down. Fixing from the inside out instead piles guesses on guesses, spreading wrong types further and leaving the unfixed spots even riskier.

Once noImplicitAny is on, you'll often see code that writes any explicitly just to get past the compiler. This fix is typical:

async function loadConfig(id: string) { const res: any = await fetchConfig(id); return res.data; }

It compiles. It means nothing. Even with strictNullChecks on, the null check on res.data is defeated. For an unknown type, unknown is better than any, because it forces you to narrow the value before using it. Your metrics should match. Instead of counting errors, watch where the remaining anys are.

strictNullChecks changes behavior

strictNullChecks is the heart of type safety and the hardest one. That's because it surfaces bugs in execution time, not compile time. The way you fix things changes too. You add branches, insert defaults, add early returns. Every one of these changes runtime behavior. Regressions don't come from the option itself — they come from these fixes.

While fixing user.name.toUpperCase(), if you insert user.name ?? 'unknown', the spot that used to blow up on null now returns a string. Once the empty string disappears and a default takes its place, the logic below receives different values. It's at the level where a reviewer has to confirm whether this is intended behavior. That's why I made it a rule to commit one per null error. Splitting commits apart makes it easy to revisit only the behavior changes later.

The most common mistake is overusing the non-null assertion !. It's great at silencing the compiler, and it's equally great at confusing people later. The ! is only truly justified in two places: a private field you can guarantee is initialized right after construction, and a spot where the library docs guarantee the value exists. Everywhere else, ? or ?? is correct. Turning on eslint's no-non-null-assertion forces ! to be surfaced in reviews.

The whole time I was enabling it, the temptation kept calling: "pause it, ship, then re-enable it." You need to hold the line there. A disabled tsconfig is easy to miss in review, and re-enabling it always falls out of the plan. CI guards the option state. I hooked tsc --noEmit into the PR check and wrote the allowed error count per option to a file, failing the build whenever the count rises above that baseline. For the cases that still remain, I use @ts-expect-error with a reason written next to it instead of @ts-ignore.

Record the error count before enabling

Measure the effect in numbers too. Record the error count before enabling an option, then record it again after. strictNullChecks looks like it produces the most errors at first, but once you fix them, little remains. noUncheckedIndexedAccess is the opposite. A single line of arr[0] makes dozens of places scream, and the fix is to sprinkle ! or ?? all over the code. If array index access is rare in your codebase, enabling it is right; if iteration and index access are part of daily life, you should first decide whether the noise is worth tolerating. If it isn't, drop it and note the reason in the commit message. Skipping one option isn't shameful. Enabling it without a reason and then failing to uphold it is worse.

Upgrading to strict in legacy code isn't an event — it's a schedule. The efficiency of gradual improvement comes from the ordering. The first move is enabling noImplicitAny. Don't clear out an entire weekend; start by committing one line in tsconfig and an error-count baseline today. The strictNullChecks day comes next. Keep enabling and fixing at the same pace next week and the month after, and one day you'll see a tsc output with no errors for the first time.

Comments

Loading comments.

Good Follow-up Reads

Posts connected to the topic you just read.

View all Tools
Tools

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.

#TypeScript#strict 모드#타입 안정성#레거시 코드
Tools

ESLint Flat Config 마이그레이션 실패 일지와 살아남는 체크리스트

ESLint 9의 flat config로 넘어가면서 extends가 사라지고, 플러그인 호환성 문제, VS Code ESLint 확장과의 설정 불일치, 글로벌 변수 선언 방식 변화 등 현장에서 마주치는 장애물을 해결 순서대로 정리한다. 삽질을 줄이는 실전 체크리스트. 2025년 4월, ESLint 9가 정식 릴리스되면서 파일은 deprecated 경고를 넘어 아예 무시되기 시작했다.

#ESLint#flat config#eslintrc#마이그레이션

Previous post

200 OK doesn't protect your zero-downtime deployment

Next post

The Page Is Already Shifting Before the Font Even Shows

DevInsight Digest

Keep every new article in one calm feed.

Follow the full publication feed without promotional alerts.

Subscribe to RSS