On this page
Enabling strict mode in an established application is a migration project, not a switch you flip and forget. The compiler exposes assumptions that previously stayed implicit: whether a lookup can fail, who initializes a field, and what an external response really contains. A useful rollout makes those assumptions explicit without burying the team in permanent escape hatches.
Inventory the boundaries before changing configuration
Start with the JSON to TypeScript tool for representative, sanitized API payloads. Generate a starting interface, compare several real responses, and mark fields that can be absent or null. Generated types describe samples; they do not prove what a server will return or validate values at runtime.
Record your compiler version, configuration inheritance, source directories, and existing diagnostic count. Pin the TypeScript version during the migration so new compiler behavior does not obscure progress. Read the official strict option reference to understand what your installed version includes in the strict family.
Choose a migration boundary with a clear owner, such as a data adapter and its tests. Shared types deserve early attention because a small correction there can eliminate many downstream errors. Keep unrelated formatting or architecture changes out of migration patches; reviewers need to distinguish safer typing from changed application behavior.
Use a separate check configuration
A second configuration lets continuous integration report strict diagnostics while the existing application build continues to work. Extending the real configuration reduces drift, but inspect inherited options, especially include, exclude, paths, and project references. A configuration file is not a sandbox that magically isolates every imported dependency.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"strict": true,
"noEmit": true
},
"include": ["src/account/**/*.ts"]
}The import graph matters: files imported by an included module can enter the program even when they do not match include. Exclude controls initial file discovery, not a prohibition on following imports. If the chosen slice pulls in the entire application, consider a real package boundary or begin with a smaller leaf module instead.
For repositories where slicing is impractical, enable individual strict-family checks across the project first. Start with the checks that produce actionable diagnostics, then tackle nullability separately if it dominates the count. Document every temporary override. The destination should be strict enabled without a forgotten collection of exceptions beneath it.
Track diagnostic trends, not just one total. Group errors by code, owning module, and underlying cause. Fixing a wrongly optional shared property may clear dozens of messages; adding dozens of assertions merely hides them. A nonblocking report can become a blocking check after a slice reaches zero errors.
Fix implicit any where information enters
An implicit any often indicates a missing contract at a function boundary. Add parameter types to exported helpers, callbacks without contextual typing, and adapters around untyped dependencies. Let local inference handle straightforward variables rather than annotating everything mechanically. More annotations are not automatically more safety.
Use unknown instead of any when a value is genuinely untrusted. Unknown forces callers to establish what operations are valid. Any silently permits property access and spreads uncertainty into code that otherwise looks checked. The TypeScript handbook explains these differences and when inference already provides enough information.
function readDisplayName(value: unknown): string {
if (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof value.name === "string"
) {
return value.name;
}
throw new Error("Profile must contain a string name");
}This function validates one field, not a complete profile schema. Keep its promise narrow. A runtime validator is useful for larger structures, but its schema must still represent the business contract. Use the JavaScript and TypeScript guides as supporting material when teammates need a shared vocabulary for boundary checks.
Treat nullability as a product decision
With strictNullChecks enabled, a missing lookup cannot masquerade as a guaranteed value. Decide what absence means before choosing a syntax fix. Should an unavailable account show an empty state, trigger a retry, or be treated as a broken invariant? These outcomes are behavior decisions, not merely compiler preferences.
const account = accounts.find(item => item.id === selectedId);
if (!account) {
showEmptySelection();
return;
}
renderAccount(account);An early return often makes the valid path easier to read. Optional chaining is appropriate when absence should propagate, while nullish coalescing supplies a default only for null or undefined. Unlike logical OR, it preserves legitimate zero, false, and empty-string values. Do not substitute defaults simply to make the diagnostic disappear.
Reserve non-null assertions for invariants you can explain and test. Repeated exclamation marks around DOM queries usually indicate a missing initialization check. Likewise, a definite-assignment assertion on a class field does not initialize that field; it transfers responsibility from the compiler to your lifecycle assumptions.
Update tests alongside these changes. Cover a failed lookup, an absent optional property, and malformed external data. The narrowing documentation shows how control flow proves safety after guards, making these tests and implementations easier to align.
Ratchet safety without freezing delivery
Once a slice passes, require that strict check in continuous integration and keep it green. Assign each remaining area an owner and an explicit next step. If suppressions are unavoidable, prefer documented exceptions with a reason and a removal condition. An expected-error directive is safer than a blanket ignore when you need the compiler to flag a now-unnecessary suppression.
Remember that strict is not every possible safety option. noUncheckedIndexedAccess and exactOptionalPropertyTypes are separate decisions. Introduce them deliberately after the main migration, with examples that explain their extra constraints. Also inspect declaration quality for dependencies: skipLibCheck can reduce declaration checking, but it does not repair misleading library types.
A practical completion definition includes a clean strict check, reviewed exceptions, tests for changed absence handling, and contributor guidance. Put that guidance near other development practices, not in a forgotten migration ticket. Future work should inherit the safer default rather than reopening the same debate on every pull request.
Key takeaways
- Establish a baseline and pin the compiler during migration.
- Migrate coherent slices while accounting for imported dependencies.
- Replace untrusted any values with unknown and runtime checks.
- Resolve nullability according to intended application behavior.
- Make completed strict checks mandatory and review remaining exceptions.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor