Measured against TypeScript 5.9.3 on 2026-09-01.
The @ts-ignore versus @ts-expect-error question has a settled answer and a lot of writing behind it: prefer @ts-expect-error, because @ts-ignore sits there in silence long after the error it hid has gone.1 We wanted to know how often that choice actually comes up, so we counted every suppression comment in five of our own production codebases.
The total was one. One @ts-expect-error, zero @ts-ignore, zero @ts-nocheck, across 287 TypeScript files and 31,128 lines, all on TypeScript 5.9.3 with strict: true. The same code contains 304 type assertions.
Three things that came out of the count:
- The one directive that exists is not the type checker being wrong about the program. It is an
importof a.css?rawfile, a bundler feature with no type declaration in scope. - The mechanism every guide is about is roughly 1 in 300 of how this code leaves its own type system. The other 300-odd are assertions, and nobody writes "when to use" guides for those.
- Exactly one opt-out in this article fails the build when it stops being necessary, and it is not the one most of these codebases would reach for by habit.
If you want the config advice rather than the count, our TypeScript best practices guide has a section on when to reach for each directive. This post is the measurement behind it.
What We Counted
We looked for four things across the five codebases: // @ts-ignore, // @ts-expect-error, // @ts-nocheck, and eslint-disable in any of its forms. @ts-nocheck disables semantic checking for an entire file and has worked in .ts files, not just JavaScript, since TypeScript 3.7.2
The codebases are ours: two backend APIs, a browser extension, a content site, and this blog. All five run TypeScript 5.9.3 and strict: true. The line basis is 287 non-generated .ts and .tsx files, 31,128 non-blank lines, scoped to what each project's tsconfig.json actually compiles, with declaration files and build output excluded.
For staleness we used the TypeScript compiler API.3 A custom CompilerHost blanks every @ts-ignore and @ts-expect-error comment out of the in-memory copy of each file, keeping byte lengths identical so every position still lines up, and then getPreEmitDiagnostics reports the errors that were being suppressed. A directive with no error under it in that view is suppressing nothing. The check was tested first against a fixture with two live directives and two stale ones, and it labelled all four correctly.
| Codebase | Lines | @ts-ignore | @ts-expect-error | @ts-nocheck | eslint-disable |
|---|---|---|---|---|---|
| Node/Hono media API | 8,118 | 0 | 0 | 0 | 0 |
| Next.js content site | 8,517 | 0 | 0 | 0 | 0 |
| Next.js blog (this site) | 6,530 | 0 | 0 | 0 | 3 |
| MV3 browser extension | 4,385 | 0 | 1 | 0 | 6 |
| Node/Hono tools API | 3,578 | 0 | 0 | 0 | 0 |
| Total | 31,128 | 0 | 1 | 0 | 9 |
The One Directive Is a Bundler Gap, Not a Type Error
The single @ts-expect-error in all five codebases sits over an import, not over a line of logic:
// @ts-expect-error -- Vite raw import
import buttonCSS from '../ui/button.css?raw'tsThe ?raw suffix is a build-tool instruction. Vite reads the file and hands the module its contents as a string.4 TypeScript knows nothing about that transform, and the type declaration that would describe it is not in scope in this project: the extension's tsconfig lists chrome and vitest/globals in its types array and nothing that covers *.css?raw. So the compiler sees an import of a module it cannot resolve a type for, and it errors. The comment is doing real work: remove it and tsc fails.
It has a reason on the same line, and it is not stale. It is also the kind of case where a suppression is arguably the wrong fix - a one-line ambient declaration for *.css?raw would type the import properly - but as written it is a legitimate use of the directive.
Zero next to it does not mean the code is clean. It means that in roughly 31,000 lines of application code under strict, the number of times the type checker was wrong about the program and had to be told to be quiet was zero. The one time a directive was needed, it was the build tool and the type checker disagreeing about what a module is, which is a different problem.
304 Assertions, One Comment
The same file set that holds one suppression comment holds 304 assertion-style opt-outs.
| Codebase | as (excl. as const) | ! non-null | explicit any |
|---|---|---|---|
| Node/Hono media API | 55 | 1 | 39 |
| Next.js content site | 7 | 1 | 0 |
| Next.js blog (this site) | 20 | 8 | 0 |
| MV3 browser extension | 88 | 9 | 0 |
| Node/Hono tools API | 75 | 1 | 0 |
| Total | 245 | 20 | 39 |
An assertion is also the type system being told to stop. It just does not announce itself the way a comment does. value as User emits no JavaScript and performs no check; it changes what the compiler believes and nothing else. ! is the same move on nullability, and an explicit any turns checking off for everything downstream of it.
Not all 304 are a problem. as const is excluded from the count. Asserting unknown from JSON.parse and then narrowing is honest work. But the category is about 300 times more common here than the comment directives, and it is the one nobody argues about. We graded these assertions by whether the value came from outside the program in an earlier audit; the short version is that the risky ones are concrete shapes asserted straight onto parsed input, and there were more of those than of every suppression comment combined.
Only One Opt-Out Tells You When It Is Stale
Of everything in this article, exactly one mechanism fails the build the moment it is no longer needed: @ts-expect-error. Here is a file with one stale @ts-expect-error, one stale @ts-ignore, and one as that changes nothing, run through tsc --strict --noEmit:
a.ts(4,1): error TS2578: Unused '@ts-expect-error' directive.
tsc exit: 2
The stale @ts-ignore produces nothing. The unnecessary as produces nothing. Only the @ts-expect-error is reported, and it fails the run.1
That is the actual argument for @ts-expect-error over @ts-ignore, and it is not about style. @ts-expect-error has a way of being noticed and removed once the code under it is fixed. @ts-ignore does not, so it accumulates. The same gap exists for assertions: @typescript-eslint/no-unnecessary-type-assertion will flag an as that does nothing, but it needs typescript-eslint's type-aware configuration to run,5 and none of the five codebases have it set up. Three of them run no ESLint at all.
The 80 @ts-ignore Comments Next.js Wrote For You
Two of the five codebases contain 80 @ts-ignore comments that nobody on the team typed. Next.js 16 generates .next/types/validator.ts to check that every route file exports the right shapes, and inside it there is one @ts-ignore per route: 12 in the blog, 28 in the content site. Next keeps a duplicate copy under .next/dev/types/, and tsconfig.json includes .next/types,6 so npm run typecheck compiles 24 and 56 of them.
Each one sits over a line like type __Unused = __Check and suppresses an unused-declaration warning that only fires under noUnusedLocals. None of our tsconfig files set that flag, so in our configuration these comments are inert; the suppression-free compile finds no error under any of them. They matter only if you turn noUnusedLocals on expecting it to police unused code in your own files, at which point you have also inherited 80 framework @ts-ignore lines that are regenerated on every build and cannot be edited out.
eslint-disable, For Completeness
The nine eslint-disable comments are all narrow, and seven of them are inert. Every one is eslint-disable-next-line, every one names a specific rule (@next/next/no-img-element three times, no-console three times, no-new-func twice, no-constant-condition once), and none is the bare form that switches off every rule on the line. Two carry a -- reason.
Seven of the nine live in the browser extension, which has no ESLint installed: no dependency, no config, no lint script. They are suppressing a linter that does not run. It is the inverse of a stale directive - not a comment that outlived its error, a comment whose checker was never there.
Where This Is Weak
- They are our own codebases, from one team. Five projects written to one style guide share habits, and shared habits show up as shared counts. A codebase migrated from JavaScript, or one where
strictwas turned on years in, would look nothing like this. Greenfield-strict is the easy case and all five are close to it. - Nothing here is large. The biggest is about 9,000 lines. Whether
@ts-ignorestays near zero at 200,000 lines and a deeper dependency tree, we cannot say from this. - Two of the five do not lint, and none run type-aware linting. "We do not suppress" is partly "nothing is checking hard enough to require it". The
@ts-ignorecount is a real property of the code; theeslint-disablecount is shaped by ESLint mostly not running. - Zero
@ts-ignoreis a team norm as much as a fact aboutstrict.strictmakes a suppression rarely necessary. It does not make one rare on its own. Deciding to fix the type instead of silencing it is a habit layered on top, and a different team with the same config would land somewhere else. - 304 assertions is not 304 bugs.
as constis already excluded, and many of the rest restate something a schema checked a few lines earlier. The number is a ceiling on how much was opted out, not a measure of risk. The grading that separates the safe ones from the dangerous ones is in the earlier audit.
Running It On Your Own Repo
The count is one command:
grep -rnE '@ts-(ignore|expect-error|nocheck)' src | grep -v node_modulesbashFor whether your @ts-ignore comments still suppress anything, change each one to @ts-expect-error and run tsc --noEmit. Every Unused '@ts-expect-error' directive in the output is an @ts-ignore that is currently hiding nothing, and would hide the next real error to land on that line. For assertions, turn on @typescript-eslint/no-unnecessary-type-assertion with type information and let it tell you which as and ! do no work.5
FAQ
When should I use @ts-ignore?
Almost never, and on this evidence you may go a long time without needing it at all. Prefer @ts-expect-error with a reason on the same line: it fails the build once the underlying error is fixed, so it gets deleted instead of rotting.1 Keep @ts-ignore only for an error that appears on some TypeScript versions you support and not others, where an expect-error would itself be reported as unused on the versions that are fine.
What is the difference between @ts-ignore and @ts-expect-error?
Both suppress the error on the next line. If there is no error, @ts-ignore does nothing and @ts-expect-error reports Unused '@ts-expect-error' directive.1 That one difference is the entire reason to prefer it: a stale @ts-expect-error breaks your build and gets removed, while a stale @ts-ignore stays and can mask a genuine error introduced later.
How many @ts-ignore comments are normal in a TypeScript project?
We found zero across 31,128 lines of strict, greenfield application code from one team. Treat that as a floor, not a target - a project migrated from JavaScript, or one leaning on untyped dependencies, will have more. If yours has dozens, the count itself is not the problem; whether each one still suppresses a real error is.
Does @ts-expect-error need a comment explaining why?
TypeScript does not require one. typescript-eslint's ban-ts-comment rule does: its recommended setting allows @ts-expect-error only with a description of at least 3 characters, raised to 10 in the strict config, and bans @ts-ignore and @ts-nocheck outright.7 The one directive in our five codebases carries a reason.
Is a type assertion safer than @ts-ignore?
No, and in one respect it is worse. A stale @ts-expect-error fails the build. A stale @ts-ignore is silent. A stale as is also silent, and there is no built-in equivalent of "unused directive" for it - you need @typescript-eslint/no-unnecessary-type-assertion with type information.5 Our five codebases hold 304 assertions and no rule checking them.
Why does my Next.js project have @ts-ignore comments I did not write?
Next generates .next/types/validator.ts to validate your route exports and puts one @ts-ignore per route inside it, and tsconfig.json includes .next/types, so those comments are compiled on every typecheck.6 Under a default strict config they suppress nothing. They only start to matter if you enable noUnusedLocals and expect it to be strict about unused code.
Disclosure
The five codebases are all our own products: two backend APIs, a browser extension, a content site, and this blog. That is the measurement's main limit. Five projects from one team share conventions, and "we rarely suppress" is partly a house style rather than only a property of strict. The commands above are the transferable part. Run them on a codebase we did not write and the numbers will be different.
Sources
Checked 2026-09-01.
Sources
-
TypeScript 3.9 release notes:
// @ts-expect-errorcomments - that@ts-expect-errorsuppresses an error on the next line and reports "Unused '@ts-expect-error' directive" when there is none, while@ts-ignoredoes nothing in that case. -
TypeScript 3.7 release notes - that
// @ts-nocheckdisables semantic checking for a whole file and was extended from JavaScript to.tsfiles in 3.7. -
TypeScript compiler API wiki -
createProgram, a customCompilerHost, andgetPreEmitDiagnostics, used for the staleness check. -
Vite: static asset handling - that importing a file with the
?rawsuffix returns its contents as a string via a build-time transform. -
typescript-eslint:
no-unnecessary-type-assertion- that the rule flags anasor!that does not change the type, and that it requires type information to run. -
Next.js: TypeScript configuration - that Next generates route types under
.next/typesand adds that path to theincludearray intsconfig.json. -
typescript-eslint:
ban-ts-comment- the recommended configuration bans@ts-ignoreand@ts-nocheck, allows@ts-expect-erroronly with a description, and sets the minimum description length to 3, or 10 in the strict config.