TypeScript does not make an application safe merely because files end in .ts. Extensive any, ignored nullability, and unchecked assertions still leave defects for users to discover. Strict mode turns the compiler into a more useful design check.
Enabling it in an older project may reveal hundreds of errors. The code did not suddenly break; the compiler is exposing assumptions that were already unverified.
What does strict enable?
{
"compilerOptions": {
"strict": true,
"noEmit": true
}
}
The flag enables a family of checks including strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, and useUnknownInCatchVariables. Future TypeScript releases may add checks to this family, so run type-checking in CI during upgrades.
Use strictNullChecks to remove unsafe assumptions
const user = users.find((item) => item.id === selectedId);
console.log(user.name); // user may be undefined
Do not automatically add !. Model the business rule:
if (!user) {
throw new Error('Selected user was not found');
}
console.log(user.name);
When a missing value is normal, return User | undefined. When it violates an invariant, validate at the boundary and use the narrower type internally.
Replace any with unknown at boundaries
any disables checking and propagates. Data from an API, storage, or message bus should begin as unknown:
function parseProfile(input: unknown): Profile {
if (!isProfile(input)) {
throw new Error('Invalid profile payload');
}
return input;
}
A type guard or schema validator narrows the value after runtime validation. TypeScript types disappear during compilation and cannot validate external data by themselves.
Handle caught values safely
try {
await saveOrder();
} catch (error: unknown) {
const message = error instanceof Error
? error.message
: 'Unknown error';
reportFailure(message);
}
JavaScript can throw any value, so checking before reading message is the honest approach.
Model states with discriminated unions
type LoadState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; message: string };
This avoids contradictory combinations of isLoading, hasError, and hasData. Switching on status narrows the available fields automatically.
Make switches exhaustive with never
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}
Use this in the default branch of a switch. Adding a new union member without handling it then becomes a compile-time error.
Validate configuration with satisfies
type RouteName = 'home' | 'account';
const routes = {
home: '/',
account: '/account',
} satisfies Record<RouteName, string>;
satisfies checks compatibility while preserving the expression's specific inferred type. It works well for route maps, configuration, and complete dictionaries.
Limit assertions
value! and value as SomeType do not add runtime checks. Prefer conditions, optional chaining, assertion functions that actually verify invariants, and validation for external data. Avoid using as unknown as T to bypass a contract.
Migrate an existing project gradually
- Create a dedicated type-check command and run it in CI.
- Start with
strictNullChecksif null errors dominate. - Fix boundary modules such as API clients, parsers, and shared types first.
- Replace boundary
anywithunknown. - Treat current failures as a backlog and allow no new ones.
- Enable all of
strict, then evaluate flags such asnoUncheckedIndexedAccess.
Do not disable strict mode across the project because one dependency has weak types. Isolate the integration behind an adapter or a narrow, documented exception.
Review checklist
- External data begins as
unknownand is validated. - Nullable values are checked before use.
- Unions represent valid states instead of boolean combinations.
- Assertions are rare, local, and justified.
- Type-checking runs in CI.
- TypeScript upgrades include release-note review and strict-error remediation.




No comments yet. Be the first to share your thoughts.