"I inherited a 120,000-line legacy Node and React codebase last quarter."
The build ran fine under loose TypeScript rules, but we were shipping raw runtime errors—like "Cannot read properties of undefined (reading 'map')"—to production twice a week. That is when I convinced our product manager to let us run a staged migration to TypeScript Strict Mode.
Many engineering teams delay migrating to strict mode because they fear it will freeze feature development or block deployment pipelines with thousands of compile errors. In 2026, the standard practice is to adopt **staged type transformations**. You do not flip the strict flag overnight; you systematically enable individual safety boundaries one compile flag at a time.
This guide outlines the exact, step-by-step order of operations to transition your legacy JavaScript systems to TypeScript strict mode incrementally, keeping your codebase shippable and your build pipeline passing at every checkpoint.
02. Phase 1: Creating the Coexistence Sandbox
Do not start by changing your file extensions to .ts. Your first goal is to establish a type-checking baseline while allowing your existing JavaScript code to build normally.
Create a baseline tsconfig.json file at the root of your project:
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"allowJs": true, // Allows JS files to coexist
"checkJs": false, // Disables strict checking for JS files during initial stage
"strict": false, // Disables strict mode for now
"noEmit": true // Runs tsc strictly for type checking
},
"include": ["src/**/*"]
}
Once this file is created, add a check task to your CI pipeline: tsc --noEmit. This guarantees that you can monitor type errors without blocking production bundles.
03. Phase 2: The Staged Order of Operations
Enabling "strict": true globally triggers all strict flags simultaneously. Instead, enable them one by one. I recommend following this exact sequence:
Step 1: Enable noImplicitAny
This is the most common compiler warning. It prevents TypeScript from assigning the fallback any type to function parameters or variables where it cannot infer the type.
For example, this insecure JS function:
// TypeScript Error: Parameter 'user' implicitly has an 'any' type.
function formatUser(user) {
return user.name.toUpperCase();
}
Resolve it by defining a strict type interface:
interface User {
name: string;
}
function formatUser(user: User): string {
return user.name.toUpperCase();
}
Step 2: Enable strictNullChecks
This flag ensures that null and undefined are handled explicitly. It prevents the notorious "Cannot read properties of undefined" runtime exceptions.
If this flag is enabled, objects that can be optionally empty must be checked before accessing properties:
interface Profile {
age?: number;
}
// Compiler Error: Object is possibly 'undefined'.
function printAge(profile: Profile) {
return profile.age.toFixed(0);
}
Correct this using type narrowing:
function printAge(profile: Profile) {
if (profile.age === undefined) {
return "Age not provided";
}
return profile.age.toFixed(0); // Safe to execute
}
04. Phase 3: Narrowing and Suppression Strategies
When migrating a massive codebase, you will hit roadblocks where you cannot solve every type error immediately.
Avoid using any as a fallback because it disables type-safety for that value completely. Instead, use unknown. It forces you to write type narrowing checks before accessing properties or executing functions:
function parsePayload(data: unknown) {
// We can't access data.id directly without check.
if (data && typeof data === "object" && "id" in data) {
// Narrowed scope permits safe execution
console.log("Valid ID:", (data as { id: string }).id);
}
}
If you must suppress a compilation error to unblock a release, use // @ts-expect-error instead of // @ts-ignore. The expect-error comment will throw a compiler warning if the code is refactored in the future and the type check passes, keeping your codebase clean.
05. Catch Variable Narrowing (useUnknownInCatchVariables)
In legacy codebases, catch variables are implicitly typed as any. In strict mode, the useUnknownInCatchVariables flag changes the type of catch variables to unknown. This prevents you from executing properties on raw errors without verifying their type:
try {
fetchUserData();
} catch (error) {
// error is typed as unknown. error.message will throw compile error.
if (error instanceof Error) {
console.error("Failed to load user:", error.message);
} else {
console.error("Unknown API error:", error);
}
}
06. The Migration Verdict
Migrating to TypeScript Strict Mode is a staged process, not a simple config toggle. It requires systematic code narrowing, strict validation patterns, and continuous integration audits.
Start today by running your healthcheck, establishing your tsconfig coexistence sandbox, and staged enabling parameters.
To test your skills in debugging strict type interfaces, check out our [INTERNAL LINK: frontend coding challenges], or join our [INTERNAL LINK: React Masterclass learning path]. You can also book [INTERNAL LINK: 1:1 expert mentorship sessions] with our core engineering team to audit your system migration plans.
07. Frequently Asked Questions
What is the difference between strict: true and individual flags?
Flipping strict: true enables all strict flags simultaneously. Setting individual flags (e.g. noImplicitAny, strictNullChecks) to true manually allows you to fix errors incrementally, flag by flag.
Why choose unknown instead of any?
The any type disables all type checks, creating potential runtime bugs. The unknown type instructs the compiler that the value is unsafe, forcing you to use type narrowing checks before accessing properties.
How should I migrate external packages without typings?
If a package has no npm @types module, create a custom declaration file (e.g. declarations.d.ts) inside your src folder and define a broad module declaration to satisfy the compiler.