# Strict mode

Source: https://codewiki.com/typescript/strict-mode/

> - **what**: TypeScript's `strict` setting is a family of compiler checks that rejects assignments, calls, and property reads that cannot be shown to be safe.
> - **trap**: `strict: true` does not validate JSON or include every hardening option, such as `noUncheckedIndexedAccess`; assertions can also bypass its findings.
> - **fix**: Enable `strict` in the project config, accept external data as `unknown`, and resolve each error with narrowing, initialization, and tests.

## What it is and why it exists

TypeScript strict mode is not a runtime mode. It is a related set of static checks. With `strict: true`, the compiler takes a more conservative approach to nullish values, function parameters, class fields, `this`, catch variables, and places where it cannot infer a type.

TypeScript has to accommodate a large amount of existing JavaScript, so projects can add type constraints gradually. A permissive configuration lets old code compile first, but it also accepts assumptions that have not been proved. A lookup might have no result, a callback might accept only a narrower input, and a class field might still be uninitialized when read.

Strict mode turns those assumptions into compiler errors so you can fix them before the code runs. You meet it when creating a `tsconfig.json`, taking over an older project, upgrading the compiler, or adding a boundary around a library with weak declarations. It does not require annotations on every local variable; code stays concise when inference has enough information.

These guarantees end at compilation. TypeScript performs type erasure, so interfaces, unions, and most checks do not remain in the emitted JavaScript. Strict mode constrains source code that the compiler sees, but it cannot prove that network responses, file contents, or JavaScript callers match their declarations.

## How it works

`strict` is an umbrella switch. TypeScript 6 enables nine related options through it, though you can still set an individual child option to `false` in the same configuration. That exception does not disable the rest of the family.

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

These are the strict options in TypeScript 6. A future version may add stricter checking under `strict`, so a compiler upgrade can produce new diagnostics.

| Option | What changes when enabled |
| --- | --- |
| `noImplicitAny` | Reports an implicit `any` produced when the compiler cannot infer a type. |
| `noImplicitThis` | Reports a `this` value that can only be given the type `any`. |
| `strictNullChecks` | Treats `null` and `undefined` as distinct types that must be handled explicitly. |
| `strictFunctionTypes` | Checks parameter and return compatibility more safely when function types are assigned. |
| `strictBindCallApply` | Checks arguments to `bind`, `call`, and `apply` against the original function signature. |
| `strictPropertyInitialization` | Requires instance fields to be initialized at their declaration or along constructor paths. |
| `strictBuiltinIteratorReturn` | Gives built-in iterators a completion return type of `undefined` instead of `any`. |
| `useUnknownInCatchVariables` | Types an unannotated `catch` variable as `unknown`, requiring narrowing before use. |
| `alwaysStrict` | Parses source as ECMAScript strict mode and emits `"use strict"` where needed. |

These options change the evidence the compiler requires. `strictNullChecks` exposes the `undefined` in a lookup function's return type; an existence check then triggers control-flow narrowing. `strictPropertyInitialization` analyzes constructor paths instead of waiting for an object to fail on its first use.

`noImplicitAny` forbids only an `any` that the compiler would silently infer. It does not ban an explicit `any`. Likewise, `useUnknownInCatchVariables` protects the default catch variable, but `catch (error: any)` still opts out. Strict mode raises the default threshold; it cannot prevent code from explicitly bypassing that threshold.

## Examples

All four programs below were type-checked with TypeScript 6.0.3 and `strict: true`, then run through `tsx` on Node 24. They cover nulls, callback parameters, runtime boundaries, and built-in iterators in that order.

### Narrow a nullable lookup result

Array `find()` can return `undefined`. Strict null checking preserves that possibility in the type, so the missing branch must be handled before account properties are read.

<!-- quick -->

```typescript
// file: nullable-user.ts
type Account = { id: number; displayName: string | null };

const accounts: Account[] = [
  { id: 1, displayName: "Ada" },
  { id: 2, displayName: null },
];

function findAccount(id: number): Account | undefined {
  return accounts.find((account) => account.id === id);
}

function labelFor(id: number): string {
  const account = findAccount(id);
  if (account === undefined) return "missing";
  return account.displayName ?? `account-${account.id}`;
}

console.log(labelFor(1));
console.log(labelFor(2));
console.log(labelFor(3));
```

```text
Ada
account-2
missing
```

<!-- /quick -->

After `account === undefined`, the compiler knows that `account` is an `Account` on the remaining path. `displayName` can still be `null`, so nullish coalescing supplies a fallback label that agrees with the data model.

There is no non-null assertion here. Writing `findAccount(id)!.displayName` would remove the diagnostic but restore a runtime failure when the ID is absent.

### Keep callback parameters safe

Function assignment has to account for every value the caller may pass. A function that accepts any `Animal` can occupy a position that receives only `Dog` values. A dog-only function cannot pretend to be the general handler.

```typescript
// file: callback-variance.ts
interface Animal {
  name: string;
}

interface Dog extends Animal {
  bark(): string;
}

type AnimalHandler = (animal: Animal) => string;
type DogHandler = (dog: Dog) => string;

const describeAnimal: AnimalHandler = (animal) => `Checking ${animal.name}`;
const describeDog: DogHandler = (dog) => `${dog.name} says ${dog.bark()}`;

const handleDog: DogHandler = describeAnimal;

// @ts-expect-error A dog-only handler cannot safely handle every Animal.
const handleEveryAnimal: AnimalHandler = describeDog;

const pixel: Dog = { name: "Pixel", bark: () => "woof" };
console.log(handleDog(pixel));
console.log(describeDog(pixel));
```

```text
Checking Pixel
Pixel says woof
```

`@ts-expect-error` is a compile-time test here: if the unsafe assignment stops producing a diagnostic, the directive itself becomes an error. The sample does not call the rejected assignment, so the output comes from the two safe calls only.

This rule is function parameter variance. Its direction is easy to reverse in your head. The deciding fact is not the animal named in the function, but which values the receiving caller may later pass.

### Validate a boundary and initialize fields

Strict checking cannot validate JSON, but it can make boundary code preserve uncertainty. This example assigns the parsed result to `unknown`, checks its object shape and domain constraint, then creates an instance whose fields are fully initialized.

```typescript
// file: retry-policy.ts
class RetryPolicy {
  constructor(
    readonly endpoint: string,
    readonly maxAttempts: number,
  ) {}
}

function parsePolicy(text: string): RetryPolicy {
  const value: unknown = JSON.parse(text);
  if (
    typeof value !== "object" ||
    value === null ||
    !("endpoint" in value) ||
    !("maxAttempts" in value) ||
    typeof value.endpoint !== "string" ||
    typeof value.maxAttempts !== "number" ||
    !Number.isInteger(value.maxAttempts) ||
    value.maxAttempts < 1
  ) {
    throw new Error("Invalid retry policy");
  }
  return new RetryPolicy(value.endpoint, value.maxAttempts);
}

for (const raw of [
  '{"endpoint":"/v1/retry","maxAttempts":3}',
  '{"endpoint":"/v1/retry","maxAttempts":0}',
]) {
  try {
    const policy = parsePolicy(raw);
    console.log(`policy=${policy.endpoint}:${policy.maxAttempts}`);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.log(`error=${message}`);
  }
}
```

```text
policy=/v1/retry:3
error=Invalid retry policy
```

The constructor parameter properties are assigned before the object is returned, so `strictPropertyInitialization` can prove that both fields exist. There is no need for a definite assignment assertion or a partly initialized instance that callers could read too early.

The `catch` variable is `unknown` because JavaScript can throw a string, an object, or `null`, not only an `Error`. The `instanceof Error` branch preserves a standard error message, while the fallback safely converts another value to text.

### Check whether an iterator is done

A built-in iterator's `next()` returns both a completion marker and a value. `strictBuiltinIteratorReturn` stops the completion value from entering the program as `any`, so code checks `done` before reading it.

```typescript
// file: iterator-result.ts
function printJobs(jobs: Set<string>): void {
  const iterator = jobs.values();

  while (true) {
    const next = iterator.next();
    if (next.done) break;
    console.log(next.value.toUpperCase());
  }
}

printJobs(new Set(["compile", "test"]));
console.log("done");
```

```text
COMPILE
TEST
done
```

After the `next.done` check, TypeScript knows that `next.value` is a `string` in the unfinished branch. Calling `jobs.values().next().value.toUpperCase()` directly would ignore the empty-set case and expose a possible `undefined` under strict checking.

This diagnostic often appears after a compiler upgrade because older versions gave built-in iterator completion values the default type `any`. The repair is to follow the iterator protocol, not to append `!` to `value`.

## Pitfalls

> **Pitfall:** Treating `strict` as a runtime validator. `JSON.parse(raw) as User`, `response.json() as User`, and arguments from JavaScript receive no structural check from strict mode.

**Fix:** accept untrusted values as `unknown`, check the container type, required fields, and domain constraints, then construct the domain object. A type assertion can record a proof already made; it cannot replace that proof.

> **Pitfall:** Adding `any`, `as Target`, non-null `!`, or definite-assignment `field!` in bulk to clear the error list. These forms suppress the uncertainty the compiler is reporting.

**Fix:** repair the source of each error by typing the boundary, narrowing a union, covering a missing branch, or initializing a field in the constructor. If an escape hatch is necessary, confine it to a small adapter and document the runtime condition it relies on.

> **Pitfall:** Assuming `strict: true` includes every strongest check. Out-of-bounds indexes, exact write semantics for optional properties, and misspelled overriding methods are not all handled by this umbrella.

**Fix:** evaluate `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, and `noImplicitOverride` against the project's contracts. They impose real migration costs, so enable them individually and confirm behavior with tests instead of describing them as parts of `strict`.

> **Pitfall:** Editing `tsconfig.json` while the actual check command does not use it. Passing one source file directly to `tsc`, or selecting another project config from the wrong directory, can make a local experiment differ from the build.

**Fix:** run an explicit project command in continuous integration, such as `tsc -p tsconfig.json --noEmit`, and inspect the final result from `tsc --showConfig -p tsconfig.json`. When the config uses `extends`, look for downstream overrides of individual strict options.

> **Pitfall:** Expecting `strictFunctionTypes` to constrain interface method syntax in the same way. For compatibility with common class and DOM hierarchies, method parameter checking remains bivariant, so a narrower method can pass through an apparently safe assignment.

**Fix:** express callback contracts as function properties, such as `handle: (event: Event) => void`, so strict function checking applies. Values coming from third-party methods still need tests over their real input range; one structural assignment is not enough.

<!-- deep -->

## Where strict mode stops

Strict mode supplies useful default checks, but it does not prove a program correct. You need to know what it leaves uncovered before deciding what a clean build actually proves.

### The umbrella is not maximum strength

The options under `strict` can evolve between TypeScript versions. TypeScript 6 includes `strictBuiltinIteratorReturn`, so an upgrade from an earlier compiler may report new errors even when `tsconfig.json` did not change. That is a result of the umbrella's design, not the compiler ignoring the config.

Projects often choose three additional hardening options outside `strict`. They are not an unconditional best configuration. Each one closes a gap for a different kind of contract.

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true
  }
}
```

`noUncheckedIndexedAccess` adds a possible `undefined` to undeclared indexed results. `exactOptionalPropertyTypes` distinguishes a missing property from an explicit `undefined` write unless the property type allows the latter. `noImplicitOverride` requires `override` when replacing a base member, exposing accidental new members caused by spelling mistakes or refactors.

These options touch different code shapes, so assess them separately. Keep tests that exercise the new boundaries after enabling them. Otherwise it is easy to restore the old behavior with assertions and lose the point of the option.

### Function properties and the method exception

Strict function checking primarily governs assignments of function types. In parameter position, a function that accepts a wider type can replace one accepting a narrower type because callers supply values from that narrow set. The opposite replacement might receive an object it cannot handle.

TypeScript preserves a compatibility exception for method syntax. In an interface, `compare(a: T, b: T): number` is a method, while `compare: (a: T, b: T) => number` is a function property; the latter gets strict parameter checking. The difference often hides in library declarations, event interfaces, and types rewritten by generators.

The exception does not mean a method call will fail. It means this option does not reject some unsafe assignments. Function properties express a stronger checking intent in a new callback API; when implementing an existing method interface, cover the static gap with wider-input tests.

### Effective values after config inheritance

Real projects often compose configurations with `extends`. A base config can enable `strict` while a downstream config disables one child option. Reviewing only the base file then gives the wrong picture of the effective checks.

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
```

```json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "strictNullChecks": false
  }
}
```

The downstream `strictNullChecks: false` overrides the umbrella's default for that option, while the other strict children remain enabled. Such an exception can help a migration, but it can also remain indefinitely and leave a team believing that null checking is complete.

`tsc --showConfig -p tsconfig.json` prints the resolved configuration and file set, making it useful for confirming inheritance, defaults, and targets. It shows what the compiler actually read and is more reliable than mentally merging several JSON files.

Continuous integration should use the same `-p` entry point. An editor may choose a nearer config, and a build tool may pass overrides. A single command ensures that green editor diagnostics and the publication gate refer to the same checks.

### Checking gaps still exist

Explicit `any` is an escape hatch that strict mode leaves open. Once a value becomes `any`, property access, calls, and assignments bypass most proof, and the unsafe type can propagate through return values. One `any` at a boundary is often harder to detect than an error inside a function.

Escape hatches are easier to review when classified by the proof they skip:

| Form | Check skipped | Evidence to add |
| --- | --- | --- |
| Explicit `any` | Most later type operations | Boundary validation and propagation scope |
| `value as Target` | This assignability decision | Runtime check before the assertion |
| `@ts-expect-error` | Known diagnostic on the next line | Expected error and removal condition |
| `skipLibCheck` | Checks between declaration files | Dependency version and integration tests |

A type assertion can also cross an assignability boundary. It is appropriate when the compiler cannot derive a fact that the program has already checked at runtime. Without that check, it merely moves risk from the diagnostic list to an execution path.

`@ts-expect-error` and `@ts-ignore` suppress a diagnostic on the following line, while `skipLibCheck` skips type checking between declaration files. Their scopes differ. They do not substitute for one another or prove that skipped declarations match the runtime library.

A clean build needs an inventory of escape hatches before it means much. A review can require each suppression to name the expected diagnostic, external dependency, and removal condition, with a test covering the path the compiler no longer checks.

### Two strict modes are not the same

`alwaysStrict` concerns ECMAScript runtime strict semantics, such as rejecting certain silent failures and legacy syntax. TypeScript's `strict` umbrella includes it, but the other options on this page mostly change static type checking.

Do not infer whether type checking is active from the presence of `"use strict"` in output. Module targets, emit behavior, and source-file form affect whether the directive needs to be written. The effective compiler config decides whether checks such as `strictNullChecks` apply.

The converse is also true. JavaScript running in ECMAScript strict mode has not necessarily passed TypeScript strict checking. The mechanisms have similar names but operate at different stages and make different guarantees.

### Preserve boundaries during gradual migration

When an older project cannot fix every strict error at once, multiple project configurations or package boundaries can stage the work. Each migration unit should be independently checkable, and a newly strict area should not spread `any` back through untyped exports.

Temporary suppressions should be searchable, explained, and tied to a removal condition. `@ts-expect-error` is more suitable than `@ts-ignore` for a known diagnostic because it fails when the error disappears. It should still sit beside one specific compatibility claim rather than cover a large section of business logic.

Fixing public inputs, outputs, and data boundaries first usually reduces downstream errors. When many leaf functions fail, the root cause is often an upstream parameter or library declaration that has already become `any`. Adding assertions to each leaf hides that propagation path.

Migration is complete when the intended config is effective, escape hatches have clear scope, and runtime tests cover important boundaries. A zero error count alone is not enough. The progressive-migration topic owns the detailed rollout; strict mode defines the target checking baseline here.

<!-- /deep -->

[Checkpoint: typescript/strict-mode](https://codewiki.com/typescript/strict-mode/#checkpoint)

## Further reading

- [TypeScript config reference: `strict`](https://www.typescriptlang.org/tsconfig/strict.html)
- [TypeScript config reference: `strictNullChecks`](https://www.typescriptlang.org/tsconfig/strictNullChecks.html)
- [TypeScript config reference: `strictFunctionTypes`](https://www.typescriptlang.org/tsconfig/strictFunctionTypes.html)
- [TypeScript config reference: `strictPropertyInitialization`](https://www.typescriptlang.org/tsconfig/strictPropertyInitialization.html)
- [TypeScript config reference: `useUnknownInCatchVariables`](https://www.typescriptlang.org/tsconfig/useUnknownInCatchVariables.html)
