Volume 4

Q201: TypeScript Strict Mode and Compiler Configuration

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

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: true is the baseline for serious TypeScript projects.
  • strictNullChecks catches missing, loading, and failed data states.
  • noImplicitAny prevents accidental untyped islands.
  • unknown is safer than any for untrusted data.
  • Suppressions should be rare, explained, and temporary.
  • Migration should be incremental and enforced by CI.

Follow-Up Questions

  • When would you use unknown instead of any?
  • How would you migrate a loose TypeScript app to strict mode?
  • What does noUncheckedIndexedAccess protect against?
  • Why is @ts-expect-error safer 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.