Wondering how to enable TypeScript strict mode on an app that already has customers? The switch is one line, "strict": true in tsconfig.json. The work is the error list it reveals, and every any and @ts-ignore that was hiding those errors. Fix them in a set order, count the suppressions before and after, and fail CI when that count rises.

What does strict mode do in TypeScript: one switch, a family of checks

Strict mode in TypeScript is a single tsconfig switch that turns on the whole family of strict compiler checks, including noImplicitAny and strictNullChecks. The compiler then rejects implicit any types and unhandled null or undefined. The checks run at compile time, so they catch mistakes before they ship, and they never check the data the app receives while it runs.

Typing is one of the engineering standards for AI-assisted teams, and one a compiler can enforce for you. TypeScript’s reference for the strict option says the flag “enables a wide range of type checking behavior that results in stronger guarantees of program correctness”, that turning it on equals enabling every option in the strict mode family, and that you can then turn individual family checks off. It also warns that “Future versions of TypeScript may introduce additional stricter checking under this flag, so upgrades of TypeScript might result in new type errors in your program.”

The family, as the full reference lists it today:

OptionWhat the reference says it doesThe failure it heads off (my reading)
noImplicitAnyIssues an error wherever TypeScript would have inferred anyA function called with the wrong shape, such as a number where a string was expected
strictNullChecksGives null and undefined their own types, so using them where a concrete value is expected is an errorA null database row read as if it were an object
strictFunctionTypesChecks function parameters more correctly (function syntax only, not method syntax)A callback handed a value it cannot handle
strictBindCallApplyChecks that call, bind and apply get the right arguments for the underlying functionA wrong argument slipped through .call()
strictPropertyInitializationErrors when a class property is declared but not set in the constructorA class field read before anything set it
noImplicitThisErrors on this expressions with an implied any typethis pointing at the wrong object inside a nested function
useUnknownInCatchVariablesTypes the catch variable as unknown instead of anyA catch (e) that reads e.message when something other than an Error was thrown
strictBuiltinIteratorReturnGives built-in iterators a return type of undefined instead of anyAn iterator’s final value used as if it were data
alwaysStrictParses files in ECMAScript strict mode and emits "use strict" for each fileMistakes that sloppy JavaScript would ignore now throw

The two that find the most bugs in an app an assistant wrote, in my reading, are noImplicitAny and strictNullChecks. Everything in the table runs when the compiler runs, with one exception: alwaysStrict emits "use strict" into the output, and the reference says ECMAScript strict mode tweaks the JavaScript engine’s runtime behavior and makes a set of errors throw instead of being silently ignored. None of it looks at a request body or a database row while the app runs, so strict typing sits beside input validation for a web app and never replaces it.

Two other things share the name. JavaScript’s "use strict" directive is a runtime mode (the FAQ below covers it), and React’s <StrictMode> component runs extra checks that are development-only and do not touch the production build.

In the Production Hardening Sprint, deliverable 10.6 (strict typing) is to “enable TypeScript strict mode and resolve errors in TypeScript applications, with an equivalent strict-checking approach for other supported stacks.”

What goes wrong without it

My reading of how this happens: when the goal is to make the red underline go away, the cheapest way is to silence it, and a coding assistant asked to “fix the build” often takes the cheapest way. When TypeScript errors are ignored everywhere, they sit in five places:

Where the error is hiddenWhat it looks like in the repoWhat it costs
Strict switched off"strict": false in tsconfig.json, or, on TypeScript before 6.0, no strict key at allThe whole family is off, so implicit any and unhandled null never show up
Suppression comments// @ts-ignore (silences every error on the next line), // @ts-expect-error (the same, but complains when the line has no error), // @ts-nocheck (top of a file, turns off semantic checks for the file)The error is still there and nobody hears it
Type escapesas any, : any, as unknown as T, and the non-null assertion value!The compiler takes your word for it; the running app does not
The build told to look awayNext.js typescript.ignoreBuildErrors: true; a Vite build script with no tsc stepProduction builds ship with type errors in them
Lint switched offA /* eslint-disable */ comment at the top of a fileThe rules that would flag new suppressions go quiet for that file

The first row changed with TypeScript 6.0: its release notes say strict “is now true by default”, and a project that relied on the old default has to set "strict": false in tsconfig.json explicitly. TypeScript 7.0 keeps that default, so on 6.0 and later a missing key hides nothing and an explicit false turns the whole family off.

The fourth row is easy to miss, because the build goes green. Next.js’s TypeScript configuration describes ignoreBuildErrors as letting production builds complete even with TypeScript errors, and says it “completely skips the TypeScript type checking step”. Vite’s TypeScript notes say Vite “only performs transpilation on .ts files and does NOT perform type checking”, leaving that to your editor and build process. One dashboard had the Next.js type and lint checks both switched off at build time, over a strict: true setup; the full story is under a green build is not a working app.

With the checker silent, the bug shows up as a production error instead, such as V8’s TypeError: Cannot read properties of undefined (reading 'x') on the page a customer happened to open. When the wrong shape comes from an API response, the same failure looks like the frontend broke after a backend change.

Twenty-one third-party apps went through my June and July 2026 audits, and across them the Maintainability & Evolvability pillar averages 61.1 out of 100, scored on all 21 of the 21. Those 21 are 11 public vibe-coded apps audited across all 12 pillars and 10 held-out apps the engine had never seen, audited blind: a selected set I chose, not a random sample or a rate for AI-built apps in general. In the same audits, Reliability & Correctness averages 31.4 out of 100, also scored on 21 of the 21. For the wider picture of this kind of debt, start with how to measure technical debt in AI code.

How to enable TypeScript strict mode and fix what it finds

TypeScript strict mode is enabled by setting strict to true in tsconfig.json and running npx tsc --noEmit. Record 4 numbers before fixing anything: total errors, errors by code, files affected, and existing suppressions. Those numbers are the baseline that later proves the errors were fixed and not hidden.

The compiler behavior below comes from the TypeScript reference and handbook. The fix order is my recommendation for codebases an assistant wrote, not a benchmark.

Turn it on and take the baseline

Work on a branch. Set "strict": true, delete any strict-family flag set to false below it, and run npx tsc --noEmit, adding -p with the path for each tsconfig in a monorepo. Set the key even on TypeScript 6.0 or later, where it is already the default, so the config states it and the verify step can show it.

Check the layout first. Vite’s current React TypeScript template has a root tsconfig.json with "files": [] and references to tsconfig.app.json and tsconfig.node.json, keeps its options in tsconfig.app.json (which sets no strict key), pins TypeScript to ~6.0.2 in package.json, and builds with tsc -b && vite build. With an empty files list, a plain tsc --noEmit at the root has nothing of its own to check (my reading). The TypeScript handbook says tsc “will not automatically build dependencies unless invoked with the --build switch”, so set strict in each referenced config and run npx tsc -b, or point at the app config with npx tsc --noEmit -p tsconfig.app.json.

# run after setting "strict": true in tsconfig.app.json
npx tsc --noEmit -p tsconfig.app.json --pretty false > tsc.log
grep -c "error TS" tsc.log                                   # total errors
grep -oE "error TS[0-9]+" tsc.log | sort | uniq -c | sort -rn # errors by code
grep "error TS" tsc.log | cut -d'(' -f1 | sort -u | wc -l     # files with errors
grep -rE "@ts-ignore|@ts-expect-error|@ts-nocheck" src | wc -l  # suppression comments
grep -rE "(as|:) any([^a-zA-Z]|$)" src | wc -l               # explicit any

Four codes come straight from the family, worded in the compiler’s own message list as TS7006 (Parameter '{0}' implicitly has an '{1}' type.), TS18047 ('{0}' is possibly 'null'.), TS18048 ('{0}' is possibly 'undefined'.) and TS18046 ('{0}' is of type 'unknown'.). Non-null assertions are counted by the lint rule in the gate section, not by grep, because ! also means “not”.

Those numbers are the “before” of the verification. skipLibCheck may stay on: the reference describes it as “Skip type checking of declaration files”, so it does not hide errors in your own .ts and .tsx files, though it skips any .d.ts file you wrote too.

Fix type errors after enabling strict: the order that shrinks the list fastest

Type errors after enabling strict are fixed fastest in 6 steps: regenerate database and API types, fix shared utilities, type the data boundaries, handle null and undefined, narrow catch variables, then component props. A non-null assertion or an as any cast lowers the count and keeps the bug.

  1. 01 Generated types first. Regenerate the database and API types so the anys that flow out of them resolve at once.
  2. 02 Shared utilities and hooks next. One fix there clears many call sites.
  3. 03 Data boundaries. Every place a database row, a fetch response or JSON.parse enters the app gets a real type or a runtime schema.
  4. 04 The empty case. Wherever a value can be null or undefined, handle it: return early, show the not-found state, or use a real default.
  5. 05 catch variables. With strict on they are unknown, so narrow with instanceof Error before reading .message.
  6. 06 Component props and event handlers last. By now most of their errors came from the steps above and have gone.

For a Supabase app, step one is a single command from Supabase’s type generation guide, npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > database.types.ts, which writes types generated from your database schema.

Every error family has an honest fix and a dishonest one, and both make the count go down:

Error familyTypical cause in generated code (my reading)The honest fixThe dishonest fix
Implicit any parameter (TS7006)A helper written without typesGive the parameter its real type: any
Possibly null or undefined (TS18047, TS18048)A find or single-row query that can return nothingHandle the empty casevalue!
unknown in catch (TS18046)catch (e) followed by e.messageif (e instanceof Error) before reading itcatch (e: any)
Untyped response or JSON.parse resultThe result used as if the shape were knownA type plus a runtime schema at the boundaryas any or as unknown as T
Database types out of dateColumns changed, types never regeneratedRegenerate the types// @ts-expect-error on every call site

The non-null assertion deserves its own line. The TypeScript handbook on the non-null assertion calls it “a special syntax for removing null and undefined from a type without doing any explicit checking”, and says that, like other type assertions, it “doesn’t change the runtime behavior of your code”.

Picture a founder who turns on strict mode and asks the coding assistant to clear the errors before a release. Many of the null errors go away because the assistant puts a ! after each lookup that can come back empty, the error count reaches zero, and CI turns green. The first customer whose record is missing then gets the same crash as before, which is why the escapes have to be counted along with the errors.

When the list is too big for one pass, split it. Turn on one flag at a time, noImplicitAny first and then strictNullChecks, or go directory by directory with project references or the open-source typescript-strict-plugin. Its README says it adds strict mode “without fixing all the errors at once”: a //@ts-strict-ignore comment at the top of a file takes that file out of strict checking, an update-strict-comments script adds the comment to every file that has a strict error, and a tsc-strict command checks the rest at compile time, because the plugin alone only shows errors in the editor. Its latest npm release, 2.4.4, dates from June 2024, and the TypeScript 7.0 announcement recommends 7.0 where language server plugins are not required, so check it against your TypeScript version before you lean on it. Write the date each directory went strict in the README.

Dead and duplicate code doubles the work, so first work out how to find unused code in a repo and delete what is clearly unused before fixing its errors.

Run type check without suppressions: the CI gate and the suppression budget

A type check without suppressions needs 3 gates: tsc --noEmit fails every pull request with a type error, a stored suppression count that CI only lets go down, and lint rules that ban @ts-ignore and explicit any. The framework build must also stop skipping type errors, or none of it protects production.

Gate one is the type check on every pull request, a standard step in CI/CD best practices. A failing check blocks the merge only when it is a required check, and GitHub’s protected branches and rulesets cover private repositories only on GitHub Pro, Team or Enterprise; on GitHub Free they cover public repositories. On a free private repository, the red check is the gate and the reviewer holds the merge.

Gate two is the budget. My working rule: keep the suppression count as one number in a text file in the repo (here .suppression-budget), and fail CI when the measured count is higher, so the budget can only go down. Lower the number in the same pull request that removes a suppression; non-null assertions stay with the lint rule in gate three.

# .github/workflows/typecheck.yml
on: pull_request
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
      - run: npm ci
      - run: npx tsc --noEmit -p tsconfig.app.json
      - run: |
          count=$(grep -rE "@ts-ignore|@ts-expect-error|@ts-nocheck|(as|:) any([^a-zA-Z]|$)" src | wc -l)
          echo "suppressions: $count, budget: $(cat .suppression-budget)"
          test "$count" -le "$(cat .suppression-budget)"

Gate three makes new suppressions loud. typescript-eslint’s ban-ts-comment rule reports @ts-ignore and @ts-nocheck by default and allows @ts-expect-error only with a description; no-explicit-any reports explicit uses of the any keyword as a type annotation; no-non-null-assertion reports the ! operator and sits in the plugin’s strict config rather than the recommended one. TypeScript 7.0 ships without an API, and its announcement says it can be run side by side with 6.0 for tools that still need some programmatic access to the compiler, such as typescript-eslint.

A @ts-expect-error with a written reason is the only form I would allow. The TypeScript 3.9 release notes explain why: if the line below it has no error, the compiler reports “Unused ‘@ts-expect-error’ directive.”, while @ts-ignore “will do nothing if the following line is error-free”.

Then close the build. Set typescript.ignoreBuildErrors to false or remove it, since false is the default. On Vite, the docs say you can run tsc --noEmit in addition to Vite’s build command for production builds; the current template’s tsc -b && vite build already does the same job.

The assistant gets the same rule in its rules file: never add any, @ts-ignore or a non-null assertion to make an error go away. Writing that rule and enforcing it in CI belongs to guardrails for AI coding agents. A reviewer then reads the count on every pull request, as part of code review best practices.

TypeScript migration checklist: from JavaScript or loose TypeScript to strict

A TypeScript migration checklist for an existing app has 9 lines: a shared tsconfig base, checkJs for remaining JavaScript, baseline counts, generated types, strict on, a suppression budget in CI, no build setting that skips type errors, updated assistant rules, and smoke tests after the fixes.

  • One tsconfig.json per package, each extending a shared base.
  • allowJs and checkJs on, so the JavaScript files that remain are checked too.
  • Baseline counts recorded: total errors, errors by code, files with errors, suppressions.
  • Generated database and API types in place and regenerated.
  • "strict": true, or a per-flag plan written down with a date for each flag.
  • Suppression budget file committed and the CI gate failing above it.
  • ignoreBuildErrors removed, and the type check part of the build script.
  • The assistant’s rules file updated: no any, no @ts-ignore, no non-null assertion to clear an error.
  • Smoke tests run after the fixes, because a type fix can change behavior.

The last line matters more than it looks: a narrowed catch or a new early return changes what the app does, so learn how to write end-to-end smoke tests and run the flows that earn money again. A team moving between major TypeScript versions reads the release notes for changed defaults first: TypeScript 6.0’s release notes made strict true by default and deprecated moduleResolution node, and TypeScript 7.0 adopts 6.0’s defaults and turns its deprecations into hard errors.

Type checking on other stacks: mypy, and the TypeScript flags that come with strict

Mypy is Python’s equivalent: install it, run mypy on the package, and tighten its checks module by module while counting # type: ignore comments. In TypeScript, verbatimModuleSyntax and moduleResolution are separate from strict: one controls how imports are emitted, the other how they are resolved. Neither is part of the strict family.

Here is how to use mypy on an existing Python codebase, following mypy’s getting started guide. Install it with python3 -m pip install mypy (it needs Python 3.10 or later), then run mypy on the package directory. Expect the first run to look clean: the guide says that on existing code mypy “will most likely report little to no errors”, because by default it does not check functions without type annotations. Add annotations, then add a [mypy] section to mypy.ini (or [tool.mypy] in pyproject.toml).

--strict turns on a defined subset of mypy’s optional checks; for mypy 2.4.0 the docs list 13 flags, from --disallow-any-generics to --extra-checks, and note the list may change over time. On an old codebase, turn individual flags on in per-module sections such as [mypy-mycode.foo.*], which take precedence over the global section; that is the same directory-by-directory move as in TypeScript (my reading of mypy’s per-module options). The suppressions to count are # type: ignore comments and ignore_missing_imports, which the command-line docs call the equivalent of adding # type: ignore to every unresolved import. Pyright is the other checker teams use; Pyright’s comparison with mypy notes that it checks all code by default, annotated or not, while mypy skips unannotated functions unless --check-untyped-defs is on.

StackThe strict switchThe suppressions to countThe clean-run command
TypeScript"strict": true in tsconfig.json (the default from 6.0)@ts-ignore, @ts-expect-error, @ts-nocheck, any, !npx tsc --noEmit, or npx tsc -b on a referenced layout
Python (mypy)--strict, or strict = True in the [mypy] section# type: ignore, ignore_missing_importsmypy on the package directory

Two tsconfig options are easy to mistake for part of strict, and neither belongs to its family. TypeScript verbatimModuleSyntax arrived in 5.0, according to the verbatimModuleSyntax reference; under it, imports and exports without a type modifier are left in the output and anything with the modifier is dropped, and it replaces the deprecated importsNotUsedAsValues and preserveValueImports. Vite’s React template turns it on, and a type imported without import type then fails with TS1484 ('{0}' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.), so expect a wave of mechanical import type fixes. For example, import { User } from './types' becomes import type { User } from './types', and a line that also imports a value marks only the type: import { type User, getUser } from './api'.

Choosing moduleResolution between node and bundler is settled by what runs the code. The moduleResolution reference lists node16 or nodenext for modern Node.js, node10 (previously node) for Node.js older than v10, and bundler for use with bundlers, which never requires file extensions on relative imports. TypeScript 6.0 deprecated node and points Node.js projects to nodenext and bundler or Bun projects to bundler; 7.0 no longer supports node at all.

How to verify it

Strict typing is verified by 6 checks: the resolved config shows strict true, tsc --noEmit passes in CI, suppression counts did not rise, the fix commits added no new suppressions, the build cannot skip type errors, and a deliberate type error on a test branch fails CI.

On a layout like Vite’s, point each command at the config that lists the app’s files (-p tsconfig.app.json), or use tsc -b.

  1. 01 Config: npx tsc --showConfig -p tsconfig.app.json prints "strict": true and no strict-family flag set to false. Keep the dated output.
  2. 02 Clean run: npx tsc --noEmit reports no errors on a clean checkout, in CI. Keep the CI log.
  3. 03 Counts: the total suppression count after is less than or equal to the count before, per pattern, in the table below. Keep the table.
  4. 04 No new escapes: search the diff of the fix branch for the suppression patterns and find zero added lines, or a written reason beside each one. Keep the search output.
  5. 05 No skipping: ignoreBuildErrors is absent or false, and the build script runs the type check. Keep the two config lines.
  6. 06 The gate bites: a deliberate type error pushed on a test branch fails CI. Keep the link to the failed run.
PatternHow it is countedBeforeAfter
@ts-ignoregrepfrom the baselinefrom the final branch
@ts-expect-errorgrepfrom the baselinefrom the final branch
@ts-nocheckgrepfrom the baselinefrom the final branch
//@ts-strict-ignore (if the plugin is used)grepfrom the baselinefrom the final branch
any (as any, : any)grepfrom the baselinefrom the final branch
Non-null assertion !the no-non-null-assertion lint rulefrom the baselinefrom the final branch
Totalsum of the rowsthe baseline totalno higher than the baseline total

One row may rise while the total falls: a @ts-ignore turned into a reasoned @ts-expect-error moves between rows, and a file opted out with //@ts-strict-ignore still counts. For Python, run the same six checks with mypy and # type: ignore. A clean check proves the types line up, not that the logic is right (my reading); the smoke tests on the checklist above cover that part.

The Production Hardening Sprint verifies deliverable 10.6 this way: “Record the strict configuration and a clean checking run without suppressing the errors being fixed.”

Where the sprint does this

Strict typing is deliverable 10.6, and its result goes into deliverable 13.1, the production readiness report, which delivers “the result for every scope item, the work completed, and its verification evidence” and is verified this way: “Account for all 123 IDs; keep failures visible until resolved and explain genuine non-applicable items.” We do not build new product features or modules, complete unfinished core features or business workflows, or rebuild core functionality that does not yet perform its intended job; those sit outside this sprint. Every deliverable and how each one is verified is in the published scope.

Common questions about strict mode and type checking

How do I turn off Strict mode?

In TypeScript, set "strict": false in tsconfig.json, or keep strict on and set a single family flag such as strictNullChecks to false below it. On TypeScript 6.0 and later you have to write "strict": false explicitly, and alwaysStrict can no longer be set to false in 7.0. React’s <StrictMode> is turned off by removing the wrapper, since its checks run only on the components inside it. JavaScript’s strict mode applies automatically to the whole of a module, with no statement needed to start it. My reading: turning one flag off while you fix its errors is a better move than turning the whole family off.

What does strict mode do in JavaScript?

JavaScript’s strict mode is, in MDN on JavaScript strict mode, “a way to opt in to a restricted variant of JavaScript”: it turns some silent errors into thrown errors, fixes mistakes that make engine optimizations difficult, and prohibits some syntax likely to be defined in future versions of ECMAScript. It is a runtime mode set by the "use strict" directive, separate from the TypeScript compiler switch, which only emits that directive through alwaysStrict.

Should I use mypy strict?

Yes for new modules; for an old codebase, turn its flags on module by module rather than all at once (my working rule). The mypy docs themselves say --strict “will probably be too aggressive” when adding static types to a large, existing codebase, and per-module sections let you set the stricter flags where the code is ready.

Which is better, Pyright or mypy?

Neither is better for every codebase; they differ in documented ways. Pyright’s own comparison says it checks unannotated code by default, where mypy needs --check-untyped-defs, that it is not unusual for it to be 3x to 5x faster than mypy on large code bases, and that mypy supports plugins while Pyright does not. The choice that matters more is that whichever one you pick runs in CI and blocks the merge.

How do I ignore missing imports in mypy?

Install the library’s stub package first if one exists (stub packages are often named types-<distribution>); if none does, set ignore_missing_imports = True in a per-module section named after the imported module, such as [mypy-somelibrary]. My working rules: keep it per module and never global, and count each one toward the suppression budget.