TL;DR: Treat strict-mode migration as a product-quality program, not a file-renaming exercise. Establish a baseline, define boundaries, migrate by dependency and risk, replace
anywith domain types, and enforce progress in CI.
Why Strict Mode Migrations Fail
Teams usually begin by changing allowJs or renaming hundreds of files. The compiler then reports thousands of errors at once, and developers either disable strict checks or add any everywhere. The repository appears to be “TypeScript” while the safety benefit remains close to zero.
The successful approach is incremental. The team chooses a bounded slice, understands the errors, fixes the data boundary, and leaves the codebase measurably safer than before. Feature work continues alongside the migration because each pull request has a small, reviewable scope.
Establish a Baseline Before Editing
Record the current build time, test pass rate, type-check error count, number of any occurrences, and the modules with the highest runtime incident rate. This turns migration from an opinion into a measurable engineering initiative.
Create a strict configuration beside the existing configuration if necessary. Start with strict: true, then document temporary exceptions explicitly. A temporary escape hatch should have an owner, a reason, and a removal issue; otherwise it becomes permanent infrastructure.
Useful boundaries include:
- external API clients and response decoders,
- database and queue adapters,
- shared domain models,
- authentication and authorization decisions,
- UI components used by many teams.
These boundaries produce more value than converting isolated utility files first.
Choose a Migration Order
Start with leaf modules that have few consumers, then move inward toward shared services and entry points. Alternatively, choose one vertical product slice and migrate its request, domain, persistence, and UI layers together. The second approach gives users visible benefits sooner.
Avoid starting with the most connected file in the repository. A shared “utils” module often creates a large blast radius and hides unrelated errors. Split it into smaller modules before migrating it.
Replace the Most Dangerous any Values
Not all any values are equally risky. Prioritize values that influence money, permissions, tenant identity, destructive actions, or external requests. Replace them with explicit domain types at the boundary, then narrow unknown data before business logic uses it.
type Account = { id: string; plan: 'free' | 'team' | 'enterprise' };
function isAccount(value: unknown): value is Account {
if (!value || typeof value !== 'object') return false;
const account = value as Record<string, unknown>;
return (
typeof account.id === 'string' && ['free', 'team', 'enterprise'].includes(String(account.plan))
);
}
unknown is often the correct intermediate type. It forces the code to prove what it received instead of allowing an unchecked assumption to travel through the system.
Handle the Hard Error Categories
strictNullChecks exposes missing loading, empty, and failure states. Do not silence these errors with non-null assertions. Model the state explicitly so the UI and the API caller both handle reality.
noImplicitAny exposes untyped parameters and callbacks. Fix the function contract at its source rather than annotating every call site. In event-driven code, type the event payload and the handler together.
strictFunctionTypes often reveals unsafe callback variance. Check whether a consumer really accepts every value the producer may send. If not, narrow the contract or introduce an adapter.
Keep the Migration Safe in CI
Use a type-error baseline only as a temporary bridge. The CI rule should never allow the count to increase. Once a package reaches zero errors, switch that package to strict enforcement. Track progress by package, ownership, and risk—not only by percentage of files renamed.
Add tests around behavior before changing types in critical modules. TypeScript can prove shape compatibility, but it cannot prove that a discount is calculated correctly or that a permission rule matches business policy.
A Practical Rollout Plan
Week one is for inventory, configuration, and baseline metrics. Weeks two and three convert boundaries and one vertical slice. Weeks four and five remove high-risk any values, add runtime validation, and improve tests. The final phase tightens CI rules and removes temporary suppressions.
Migration Checklist
- Baseline errors, tests, build time, and risky
anyusage. - Define ownership and a removal date for every exception.
- Migrate data boundaries before internal consumers.
- Prefer
unknownplus validation overany. - Model null, loading, and failure states explicitly.
- Prevent new errors while reducing old ones.
- Review the migration as production engineering, not formatting.
A Worked Boundary Example
Imagine an order service that receives JSON from a payment provider. The weakest migration types the payload as PaymentEvent immediately and trusts every field. The stronger migration treats the payload as unknown, validates its signature and shape, then exposes a small internal type to the rest of the system.
type PaymentSucceeded = {
kind: 'payment.succeeded';
paymentId: string;
orderId: string;
amount: number;
currency: string;
};
function parsePaymentEvent(input: unknown): PaymentSucceeded {
if (!input || typeof input !== 'object') throw new Error('Invalid event');
const value = input as Record<string, unknown>;
if (value.kind !== 'payment.succeeded') throw new Error('Unsupported event');
if (typeof value.paymentId !== 'string' || typeof value.orderId !== 'string') {
throw new Error('Invalid identifiers');
}
if (typeof value.amount !== 'number' || typeof value.currency !== 'string') {
throw new Error('Invalid payment values');
}
return value as PaymentSucceeded;
}
In a production system, use a schema validator for this boundary and test malformed payloads. The important design decision is that untrusted data becomes a trusted domain value only after a check. This is where TypeScript improves architecture rather than merely annotating variables.
Working with Third-Party Libraries
Old JavaScript dependencies may have incomplete or inaccurate declarations. Do not spread casts throughout the application. Create one adapter with a narrow interface, add tests for the behavior you rely on, and keep the workaround in that adapter. When the dependency is upgraded, only one boundary needs review.
For internal packages, publish types alongside the implementation and enable declaration generation. A package that exports untyped values simply moves the migration problem to every consumer.
Measuring the Real Outcome
Track more than the number of .ts files. Useful measures include escaped runtime type errors, API validation failures, time spent debugging shape mismatches, percentage of critical paths covered by strict checks, and the number of unsafe assertions in security-sensitive modules.
Review the metrics monthly. If the error count falls but the number of as any casts rises, the migration is moving in the wrong direction. If build time becomes a problem, use project references or package-level checks rather than weakening strictness.
Common Failure Modes
The big-bang conversion: thousands of errors make prioritization impossible. Fix it by choosing a package or vertical slice.
The permanent baseline: a baseline hides new errors. Fix it by failing CI whenever the count increases and assigning owners to old errors.
Types without runtime checks: compile-time types do not validate HTTP, database, queue, or local-storage input. Fix it with boundary schemas.
Overly clever types: a type that requires a page of explanation is often harder to maintain than a small explicit model. Prefer types that make common code obvious.
Ignoring tests: a successful type-check does not prove behavior. Preserve regression tests around billing, permissions, parsing, and state transitions.
Final Migration Checklist
- Baseline errors, tests, build time, and risky
anyusage. - Define ownership and a removal date for every exception.
- Migrate data boundaries before internal consumers.
- Prefer
unknownplus validation overany. - Model null, loading, and failure states explicitly.
- Prevent new errors while reducing old ones.
- Add adapters around weak third-party types.
- Track runtime outcomes, not only file extensions.
- Review the migration as production engineering, not formatting.
If the codebase is business-critical, a staged audit is usually safer than a rewrite. Our full-stack web development services help teams introduce typed boundaries, tests, and migration gates while continuing feature delivery.

