Q201: TypeScript Strict Mode and Compiler Configuration
What Interviewers Want To Evaluate
Interviewers want to know whether you treat TypeScript as a design tool, not only as syntax.
They check if you understand strict mode, null safety, implicit any, compiler flags, migration strategy, library boundaries, build speed, and how configuration affects application reliability.
Short Interview Answer
TypeScript strict mode enables a set of compiler checks that make unsafe assumptions visible earlier. In production code, I prefer strict mode because it reduces runtime surprises around nulls, unknown values, function variance, and accidental any. I introduce it with a planned migration, module-by-module cleanup, better API types, and CI checks so the team improves safety without blocking delivery.
Detailed Interview Answer
TypeScript configuration is part of architecture.
It decides:
how much safety the compiler provides
how much JavaScript interop is allowed
how strict API boundaries are
how easy refactors are
how noisy migrations become
how fast local feedback stays
The most important senior habit is choosing strictness intentionally.
Strict Mode
The key setting is:
{
"compilerOptions": {
"strict": true
}
}
This enables multiple checks as a group.
Commonly important checks include:
noImplicitAny
strictNullChecks
strictFunctionTypes
strictBindCallApply
strictPropertyInitialization
noImplicitThis
useUnknownInCatchVariables
alwaysStrict
You can enable strict mode and still tune specific flags during migration, but the long-term target should be clear.
Why strictNullChecks Matters
Without strict null checks, nullable values can flow through the code silently.
type User = {
name: string;
};
function renderName(user: User | null) {
return user.name.toUpperCase();
}
With strict null checks, TypeScript forces the missing branch.
function renderName(user: User | null) {
if (!user) {
return "Guest";
}
return user.name.toUpperCase();
}
This is especially important in React apps where data may be loading, missing, unauthorized, or failed.
noImplicitAny
Implicit any removes the value of TypeScript at the exact place where clarity is needed.
function groupBy(items, getKey) {
return items.reduce((groups, item) => {
const key = getKey(item);
groups[key] ??= [];
groups[key].push(item);
return groups;
}, {});
}
This function looks generic, but it has no useful type contract.
A better version makes the relationship explicit.
function groupBy<TItem, TKey extends string>(
items: TItem[],
getKey: (item: TItem) => TKey,
) {
return items.reduce<Record<TKey, TItem[]>>((groups, item) => {
const key = getKey(item);
groups[key] ??= [];
groups[key].push(item);
return groups;
}, {} as Record<TKey, TItem[]>);
}
Now callers get safer autocomplete and fewer runtime assumptions.
exactOptionalPropertyTypes
This setting is often useful in mature projects.
{
"compilerOptions": {
"exactOptionalPropertyTypes": true
}
}
It distinguishes omitted properties from explicitly undefined values.
type CardOptions = {
subtitle?: string;
};
With stricter optional handling, APIs become more precise.
This helps when serializing JSON, merging props, or detecting whether a value was intentionally provided.
noUncheckedIndexedAccess
This setting makes index access honest.
{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}
Without it, this can look safe:
const first = users[0];
first.name;
But arrays may be empty.
The safer version handles absence.
const first = users[0];
if (!first) {
return "No users";
}
return first.name;
It can feel noisy at first, but it catches real production bugs.
unknown Instead Of any
Use unknown when data exists but is not trusted yet.
async function loadConfig(): Promise<unknown> {
return fetch("/api/config").then((res) => res.json());
}
Then validate before use.
function isConfig(value: unknown): value is { theme: string } {
return (
typeof value === "object" &&
value !== null &&
"theme" in value &&
typeof value.theme === "string"
);
}
unknown keeps the unsafe boundary visible.
tsconfig In Applications
A practical application config usually cares about:
strict checks
module resolution
jsx mode
path aliases
incremental builds
no emit when framework handles output
library DOM types
test environment types
Example:
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"incremental": true,
"moduleResolution": "bundler",
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
The exact values depend on the framework, but the intent should be documented.
Migration Strategy
For an existing JavaScript or loose TypeScript codebase, do not flip every strict flag blindly.
A senior migration plan:
measure current errors
turn on strict in CI for new packages first
fix shared types and API clients early
convert leaf modules before core modules
replace any with unknown at unsafe boundaries
add runtime validation for external data
track remaining suppressions
prevent new implicit any
The goal is steady safety, not a heroic one-time cleanup.
Suppression Policy
Sometimes you need a temporary suppression.
Prefer:
// @ts-expect-error Legacy SDK type is incorrect until sdk v4 migration.
legacyClient.send(payload);
Avoid:
// @ts-ignore
legacyClient.send(payload);
@ts-expect-error fails when the error disappears, so stale suppressions get removed.
Type Checking And Build Time
Strict checks can increase feedback time in large projects.
Common mitigations:
incremental compilation
project references
separate typecheck command in CI
editor type acquisition hygiene
avoiding huge generated union types
keeping shared types small
Type safety should be fast enough that developers actually use it.
Common Mistakes
Common mistakes include:
using any to silence errors
turning strict off for convenience
trusting API JSON without validation
using non-null assertions everywhere
forgetting test and tooling types
copying tsconfig from another framework blindly
The dangerous pattern is hiding uncertainty instead of modeling it.
Senior Trade-Offs
Strictness has cost.
It requires better modeling, better migration discipline, and sometimes more code.
But the payoff is large:
safer refactors
clearer contracts
better autocomplete
fewer impossible states
fewer production null bugs
more confident code review
I would accept some local verbosity to improve system-wide confidence.
Interview Framing
In an interview, connect strict mode to reliability.
Say:
Strict TypeScript makes invalid assumptions visible before runtime.
I enable it by default for new code.
For legacy code, I migrate in phases, protect new code from regressions, and use unknown plus runtime validation at external boundaries.
That answer shows practical judgment.
Revision Notes
strict: trueis the baseline for serious TypeScript projects.strictNullCheckscatches missing, loading, and failed data states.noImplicitAnyprevents accidental untyped islands.unknownis safer thananyfor untrusted data.- Suppressions should be rare, explained, and temporary.
- Migration should be incremental and enforced by CI.
Follow-Up Questions
- When would you use
unknowninstead ofany? - How would you migrate a loose TypeScript app to strict mode?
- What does
noUncheckedIndexedAccessprotect against? - Why is
@ts-expect-errorsafer than@ts-ignore? - How does strict mode help React application reliability?
How I Would Answer This In A Real Interview
I would say that strict mode turns TypeScript from documentation into a real safety net. I use it by default, especially with strictNullChecks and noImplicitAny, because most frontend bugs come from bad assumptions about data, loading states, and API boundaries. For legacy code, I would migrate gradually, validate external data, prefer unknown over any, and track suppressions so the codebase gets safer without freezing product work.