Every TypeScript project I audit has "strict": true in the tsconfig.json. The same project has // @ts-ignore scattered through the code, any in critical spots, and no one on the team who can explain what the flag protects.

"strict": true turns on a set of compiler flags. Each flag exists because a category of bug reached production before someone wrote the test that would have caught it. The flag map trades the compiler's complaints for an inventory of risk in your code.

What "strict": true enables

strictNullChecks

Without it, null and undefined fit into any type. With it, the compiler charges you for handling absence at the point where a value can be missing.

// without strictNullChecks: compiles, breaks at runtime
function getUser(id: string): User {
  return db.find(id) // may return undefined
}
const name = getUser('123').name // TypeError: Cannot read property 'name' of undefined

// with strictNullChecks: the compiler points at the line
function getUser(id: string): User | undefined {
  return db.find(id)
}
const user = getUser('123')
const name = user?.name

The flag reference describes the assignability rule. The TypeError exceptions that survive to production and that I can trace back to their origin are almost always a property read on an absent value.

noImplicitAny

Without it, an untyped function parameter becomes any with no warning and checking switches off for the whole function. With it, the compiler demands a declared type.

// without noImplicitAny: param is any by default
function process(data) {
  return data.value.toUpperCase() // nothing is checked
}

// with noImplicitAny: the compiler asks for the type
function process(data: { value: string }) {
  return data.value.toUpperCase()
}

The flag documentation is short. During a migration it is the one that breaks files en masse: any file without declared types fails at once.

strictFunctionTypes

Compares function types by the correct variance of parameters. Without the flag, the TypeScript accepts either direction; with it, a handler that expects MouseEvent does not fit a slot that takes Event, because the caller may pass a plain Event and the handler breaks.

type Handler = (event: Event) => void
const myHandler: Handler = (event: MouseEvent) => {
  // strictFunctionTypes points at the error
}

Method declarations (foo() {}) stay bivariant under the specification. Write callbacks as function properties when you want the check. The flag reference explains the method exception.

strictPropertyInitialization

Requires class properties to receive a value in the constructor. Prevents a read of a field the compiler treated as ready and that arrives as undefined at runtime.

class UserService {
  private db: Database // error: no value in the constructor

  constructor() {}
}

// correct: dependency injected through the constructor
class UserService {
  private db: Database

  constructor(db: Database) {
    this.db = db
  }
}

// injection outside the constructor, with definite assignment assertion
class UserService {
  private db!: Database
}

The ! operator records the intent and switches the check off for that field. The flag documentation covers the operator.

Two flags outside strict worth the cost

noUncheckedIndexedAccess

Index access returns T | undefined instead of T. Catches the bug of assuming users[0] exists, common when the list comes from an API filter.

// tsconfig.json
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }

const items = ['a', 'b', 'c']
const first = items[0] // string | undefined
const safe = first?.toUpperCase()

The flag adds noise to for loops over arrays and to maps with dynamic keys. The team pays that cost once and stops receiving undefined at runtime. Flag documentation.

exactOptionalPropertyTypes

Splits { prop?: string } (the field may be absent) from { prop: string | undefined } (the field arrives, with value undefined). Without the flag, the TypeScript treats both shapes as equal and you write a null guard on a field the API sends as present. Flag documentation.

Migrating a legacy project to strict

Turning on "strict": true in one pass across a 200k-line monorepo stalls the team for weeks. The path I use:

  1. Turn on "strict": true and cover the errors with // @ts-ignore. Accept the debt as temporary and visible.
  2. Install @typescript-eslint/ban-ts-comment and configure it to accept @ts-expect-error and reject @ts-ignore. The legacy code stays in the repository; new code that silences the compiler has to justify itself with the expected-error comment, which the compiler validates. @ts-ignore validates nothing.
  3. Resolve the silencers file by file, starting with the modules that take the most production traffic.

New code gets strict on day one. The legacy code joins as each file is fixed, and the count of @ts-expect-error comments in the repository becomes the progress number the team watches.

The cost of silencing the compiler

Every // @ts-ignore, every as any, every flag turned off is a bet: you trust your own reading of the situation more than the check at that point. On an external library with bad types, the bet is fair. On internal code, the bug the flag would have caught returns at runtime, and the error report arrives weeks later, far from the line that caused it.

When the compiler points at a line and you do not understand why, read the error and fix the type. The narrowing chapter of the handbook covers the undefined and conditional-type situations that show up most often.

References