Strict Mode and Best Practices

Introduction

strict in tsconfig.json is not a single switch—it turns on a family of checks that catch most classes of bugs before production. This chapter explains each major flag, optional stricter options, and day-to-day habits that keep TypeScript codebases maintainable as they grow.

Prerequisites

Start with strict: true

json
{
  "compilerOptions": {
    "strict": true
  }
}

Equivalent to enabling (among others):

  • strictNullChecks
  • noImplicitAny
  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization
  • noImplicitThis
  • alwaysStrict

New projects should keep strict: true unless you are migrating legacy JavaScript with a phased plan.

strictNullChecks

Forces explicit handling of null and undefined:

typescript
function length(name: string): number {
  return name.length;
}
 
const input: string | null = getName();
// length(input);  // Error
 
function safeLength(name: string | null): number {
  return name?.length ?? 0;
}

Practice: Use ? for optional properties, | null when absence is intentional, and ?? / narrowing at boundaries (API, DOM, JSON.parse).

noImplicitAny

Parameters and untyped locals cannot silently become any:

typescript
// function log(value) {  // Error under noImplicitAny
//   console.log(value);
// }
 
function log(value: unknown): void {
  console.log(value);
}

Practice: Prefer unknown over any for external data; add types at function boundaries.

strictFunctionTypes

Function parameters are checked contravariantly for callbacks—stricter than loose JavaScript:

typescript
type Handler = (event: Event) => void;
 
function listen(handler: Handler): void {
  document.addEventListener("click", handler);
}
 
// listen((e: MouseEvent) => { ... });  // May error: parameter too specific

Practice: Match library callback signatures; do not narrow parameter types when assigning to wider handlers.

strictPropertyInitialization

Class fields must be assigned in the constructor (or at declaration):

typescript
class User {
  id: string;
  name: string;
 
  constructor(id: string, name: string) {
    this.id = id;
    this.name = name;
  }
}

Use definite assignment ! only when you truly initialize elsewhere (see nullability chapter).

Optional Stricter Flags

OptionEffect
noUncheckedIndexedAccessarr[i] is T | undefined
exactOptionalPropertyTypesDistinguish missing vs undefined on optional props
noImplicitReturnsAll code paths must return in typed functions
noFallthroughCasesInSwitchPrevent accidental switch fall-through
noUnusedLocals / noUnusedParametersDead code errors (often ESLint too)
noImplicitOverrideRequire override keyword on subclasses

Example noUncheckedIndexedAccess:

typescript
const scores: number[] = [90, 85];
const first = scores[0];
// first: number | undefined

Coding Practices That Pair with strict

1. Model domains with types, not comments

typescript
type OrderStatus = "pending" | "paid" | "shipped";
 
interface Order {
  id: string;
  status: OrderStatus;
}

2. Avoid any and double assertions

typescript
// Bad: escape hatch
const data = JSON.parse(raw) as any;
 
// Better
const data: unknown = JSON.parse(raw);

3. Prefer discriminated unions over flags

typescript
type State =
  | { status: "loading" }
  | { status: "ok"; data: string[] }
  | { status: "error"; message: string };

4. Use readonly where data should not mutate

typescript
interface Config {
  readonly apiUrl: string;
}

5. Export explicit types at module boundaries

typescript
export function createUser(input: CreateUserInput): User {
  // ...
}

Migrating Legacy JavaScript

Phased approach:

  1. Enable allowJs + checkJs on a folder
  2. Fix errors file by file
  3. Rename to .ts and tighten types
  4. Turn on strict when any is mostly gone
json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "strict": false
  }
}

Temporary strict: false is acceptable during migration; track removal in tickets.

Warning

Do Not Disable strict Permanently for Convenience

Fixing types at the source pays off. Disabling strictNullChecks to silence errors often hides production crashes.

Team Conventions Checklist

  • strict: true in root tsconfig
  • CI runs tsc --noEmit or npm run typecheck
  • No new any without review comment
  • Shared tsconfig.base.json for packages
  • ESLint @typescript-eslint aligned with TS version
  • unknown at JSON/network boundaries

FAQ

Can I enable strict flags one by one?

Yes. strict is a bundle; you can enable strictNullChecks first in legacy codebases.

strict slows development?

Short-term friction, long-term fewer bugs and better editor support.

Use both—overlap is fine. tsc owns types; ESLint owns style and some logic rules.

Should tests use the same strict config?

Yes—extend the same base tsconfig with test-specific include.

any in third-party types?

skipLibCheck: true skips checking .d.ts in node_modules; your code should still stay strict.

Is strict enough for security?

No. Types do not replace validation, authz, or sanitization at runtime.