Skip to content
C Codeloom
TypeScript

TypeScript Strict Mode and tsconfig Tips

Understand what strict mode actually turns on, which extra flags are worth enabling, and how to adopt strictness incrementally.

·3 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • What strict enables
  • Useful extra flags
  • Path aliases
  • Module resolution
  • Incremental adoption

Prerequisites

  • Comfortable with JS

What and Why

"strict": true in tsconfig.json is a meta-flag that turns on a family of safety checks. Each one closes a class of bugs: implicit any, unchecked null, unsound function parameters, uninitialized class fields. Combined, they push your code toward the guarantees that make TypeScript worth using in the first place.

If you skip strict mode, you are paying TypeScript’s costs (build step, learning curve) without collecting the benefits. The goal of this article is to demystify what strict actually does and which neighbouring flags reward the effort.

Mental Model

Think of TypeScript’s checks as a dial. At the loosest end, the compiler trusts you and emits anything. At the strictest end, every uncertainty is a compile error. Strict mode parks the dial at a pragmatic level: strict enough to be useful, loose enough that most real code compiles after a cleanup pass.

  loose  --------------------------------- strict
 |                                         |
 noImplicitAny ---- strictNullChecks ---- noUncheckedIndexedAccess
              |
      strictFunctionTypes  strictPropertyInitialization
The strict mode dial: each flag tightens a different category of check

Hands-on Example

A reasonable starting tsconfig.json for a modern project:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"]
}

noUncheckedIndexedAccess makes arr[i] return T | undefined, forcing you to handle holes:

const first = items[0];
if (first) first.toUpperCase();

exactOptionalPropertyTypes distinguishes { x?: string } from { x: string | undefined }, which matters when a property’s presence is meaningful.

For libraries, add declaration: true and declarationMap: true so consumers get autocomplete and jump-to-source. For apps, set noEmit: true and let your bundler handle output.

Common Pitfalls

A few sharp edges to watch for:

  • skipLibCheck off. Without it, broken type packages from node_modules can block your build for no benefit.
  • Mismatched module and moduleResolution. Modern bundlers expect "bundler"; Node CJS expects "node16" or "nodenext". Pick the pair that matches your runtime.
  • Path aliases that the bundler does not know about. TypeScript resolves @/foo, but Webpack or Vite need separate config.
  • any leaking through casts. Strict mode does not stop you from writing as any. Linters can flag this.
  • Turning strict on all at once on a legacy codebase. You will drown. Enable one flag at a time.

Best Practices

Adopt strict mode incrementally. Start with noImplicitAny, then strictNullChecks (the biggest win and the biggest workload), then the rest. Each PR should turn on one flag and fix the resulting errors so reviewers can follow.

Keep two configs if needed: a strict tsconfig.json for new code and a tsconfig.legacy.json that loosens specific files via include. Aim to delete the legacy file over time.

Enable noUncheckedIndexedAccess once your codebase is null-safe; it catches subtle holes in maps and arrays. Add verbatimModuleSyntax to keep import elision predictable. Pair TypeScript with ESLint rules like @typescript-eslint/no-explicit-any and no-floating-promises to cover gaps the type system does not.

Finally, run tsc --noEmit in CI. The IDE can drift from the command-line compiler, especially around project references; CI is the source of truth.

Wrap-up

Strict mode is the floor, not the ceiling. Turn it on, add a couple of complementary flags, and let the compiler do its job. The first cleanup hurts; everything after is faster, safer, and easier to refactor.