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:
| Option | What the reference says it does | The failure it heads off (my reading) |
|---|---|---|
noImplicitAny | Issues an error wherever TypeScript would have inferred any | A function called with the wrong shape, such as a number where a string was expected |
strictNullChecks | Gives null and undefined their own types, so using them where a concrete value is expected is an error | A null database row read as if it were an object |
strictFunctionTypes | Checks function parameters more correctly (function syntax only, not method syntax) | A callback handed a value it cannot handle |
strictBindCallApply | Checks that call, bind and apply get the right arguments for the underlying function | A wrong argument slipped through .call() |
strictPropertyInitialization | Errors when a class property is declared but not set in the constructor | A class field read before anything set it |
noImplicitThis | Errors on this expressions with an implied any type | this pointing at the wrong object inside a nested function |
useUnknownInCatchVariables | Types the catch variable as unknown instead of any | A catch (e) that reads e.message when something other than an Error was thrown |
strictBuiltinIteratorReturn | Gives built-in iterators a return type of undefined instead of any | An iterator’s final value used as if it were data |
alwaysStrict | Parses files in ECMAScript strict mode and emits "use strict" for each file | Mistakes 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 hidden | What it looks like in the repo | What it costs |
|---|---|---|
| Strict switched off | "strict": false in tsconfig.json, or, on TypeScript before 6.0, no strict key at all | The 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 escapes | as 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 away | Next.js typescript.ignoreBuildErrors: true; a Vite build script with no tsc step | Production builds ship with type errors in them |
| Lint switched off | A /* eslint-disable */ comment at the top of a file | The 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.
- 01 Generated types first. Regenerate the database and API types so the
anys that flow out of them resolve at once. - 02 Shared utilities and hooks next. One fix there clears many call sites.
- 03 Data boundaries. Every place a database row, a
fetchresponse orJSON.parseenters the app gets a real type or a runtime schema. - 04 The empty case. Wherever a value can be
nullorundefined, handle it: return early, show the not-found state, or use a real default. - 05
catchvariables. With strict on they areunknown, so narrow withinstanceof Errorbefore reading.message. - 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 family | Typical cause in generated code (my reading) | The honest fix | The dishonest fix |
|---|---|---|---|
Implicit any parameter (TS7006) | A helper written without types | Give the parameter its real type | : any |
Possibly null or undefined (TS18047, TS18048) | A find or single-row query that can return nothing | Handle the empty case | value! |
unknown in catch (TS18046) | catch (e) followed by e.message | if (e instanceof Error) before reading it | catch (e: any) |
Untyped response or JSON.parse result | The result used as if the shape were known | A type plus a runtime schema at the boundary | as any or as unknown as T |
| Database types out of date | Columns changed, types never regenerated | Regenerate 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.jsonper package, each extending a shared base. allowJsandcheckJson, 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.
ignoreBuildErrorsremoved, 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.
| Stack | The strict switch | The suppressions to count | The 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_imports | mypy 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.
- 01 Config:
npx tsc --showConfig -p tsconfig.app.jsonprints"strict": trueand no strict-family flag set tofalse. Keep the dated output. - 02 Clean run:
npx tsc --noEmitreports no errors on a clean checkout, in CI. Keep the CI log. - 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.
- 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.
- 05 No skipping:
ignoreBuildErrorsis absent orfalse, and the build script runs the type check. Keep the two config lines. - 06 The gate bites: a deliberate type error pushed on a test branch fails CI. Keep the link to the failed run.
| Pattern | How it is counted | Before | After |
|---|---|---|---|
@ts-ignore | grep | from the baseline | from the final branch |
@ts-expect-error | grep | from the baseline | from the final branch |
@ts-nocheck | grep | from the baseline | from the final branch |
//@ts-strict-ignore (if the plugin is used) | grep | from the baseline | from the final branch |
any (as any, : any) | grep | from the baseline | from the final branch |
Non-null assertion ! | the no-non-null-assertion lint rule | from the baseline | from the final branch |
| Total | sum of the rows | the baseline total | no 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.
Owning an app means being able to run it, change it and recover it without guessing. The sprint below leaves you with the runbooks and documentation to do that.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase