Skip to main content
ASoc
Tutorial

TypeScript Ignore Next Line: Why This Repo Has One @ts-expect-error and Zero @ts-ignore

Suppress a TypeScript error on one line the safe way. Measured TS2578, TS2790 and ban-ts-comment output, the multi-line trap, and a cast that avoids the directive.

The ASoc Team7 min read

To ignore the next line in TypeScript, put // @ts-expect-error — reason directly above it. It suppresses the compiler error on that single line, and it fails the build if the line stops erroring. // @ts-ignore does the same silently and never complains. This codebase has zero @ts-ignore and exactly one @ts-expect-error, and the reasons are measured below.

The short answer

// @ts-ignore// @ts-expect-error
Suppresses the error on the next lineYesYes
Complains when the next line has no errorNoYes — TS2578: Unused '@ts-expect-error' directive
Passes this repo's lint configNoOnly with a description of 3+ characters
Covers a multi-line statementOnly its first lineOnly its first line
Right forAlmost nothingA deliberate, documented type violation

The one place this repo uses it is a webhook test, src/lib/__tests__/webhook.test.ts, line 160:

const fixture = structuredClone(
  orderCreatedFixture,
) as typeof orderCreatedFixture;
// @ts-expect-error — test fixture mutation for the unattached-order path
delete fixture.meta.custom_data.user_id;

The fixture is a LemonSqueezy order_created payload. The test needs a purchase that arrives with no custom_data.user_id, so the webhook handler stores it as unattached and fires the alert callback. user_id is a required key in the JSON's inferred type, so TypeScript refuses to delete it.

What each directive actually does

Everything below was run in this repository against TypeScript 5.9.3 and typescript-eslint 8.62.0, in a scratch file with strict: true from the repo's tsconfig.json.

Deleting a required property, with no directive:

error TS2790: The operand of a 'delete' operator must be optional.

Now the four cases that matter:

type Fixture = { meta: { custom_data: { user_id: string } } };
declare const f: Fixture;

// @ts-ignore
delete f.meta.custom_data.user_id;            // suppressed

// @ts-expect-error — mutation
delete f.meta.custom_data.user_id;            // suppressed

// @ts-expect-error — nothing wrong here, directive is stale
const ok: number = 1;                         // TS2578 on the directive

// @ts-expect-error — only the NEXT line is covered
const multi = {
  a: 1,
  b: "x" as number,                           // TS2352 — NOT suppressed
};

tsc reported exactly two errors: TS2578: Unused '@ts-expect-error' directive on the stale one, and TS2352 on the object literal's third line. The two real violations were silenced; the stale directive and the second-line error were not.

That is the whole difference, and it is the reason to prefer one over the other. A @ts-ignore never reports itself unused, so when someone later fixes the types, the comment stays behind and hides the next genuine error on that line. A @ts-expect-error turns red the moment it stops being true, so cleanup is forced rather than remembered.

"Next line" means the next physical line

The directive attaches to the line after the comment, not the statement. In the multi example above, the comment sat over const multi = { and the error was on b: "x" as number, two lines lower. It was not suppressed.

Two ways to deal with that:

  • Move the directive to sit directly above the offending line inside the literal.
  • Better, restructure so the violation is on a line of its own, such as assigning the bad value to a named constant first.

JSX needs a different comment shape

Inside JSX children a // comment is text, not a comment. Use a braced block comment:

export const X = () => (
  <div>
    {/* @ts-expect-error — foo is not a div prop */}
    <div foo="1" />
  </div>
);

This compiled clean in the same scratch run, with the invalid foo prop suppressed and no TS2578.

What this repo's lint config enforces

eslint.config.mjs extends eslint-config-next's typescript preset, which brings in typescript-eslint's ban-ts-comment rule. Running it over a scratch file produced:

4:1  error  Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free  @typescript-eslint/ban-ts-comment

So @ts-ignore fails npm run lint outright, and CI runs lint on every push. A bare @ts-expect-error fails too:

2:1  error  Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer  @typescript-eslint/ban-ts-comment

A one-character description written as // @ts-expect-error: x also passed in the same run, so the rule checks that a description exists, not that it is any good. The review habit that closes the gap is the one in webhook.test.ts: say what you are bypassing and why, so the next reader can judge whether it is still necessary.

Before you suppress, can you cast?

Most suppressions are really "I know something the types don't." A suppression says "ignore everything on this line"; a cast says one specific thing. Here is the same fixture mutation with no directive, checked with tsc --noEmit and ESLint in the same scratch run:

delete (
  fixture.meta.custom_data as Partial<typeof fixture.meta.custom_data>
).user_id;

Both passed with no output. The cast is narrower, because it only widens custom_data to optional keys and leaves the rest of that line checked. The repo keeps the directive in the test anyway, because the comment documents intent in one line. Either is defensible; what is not defensible is a suppression with no reason attached.

The same logic applies to eslint-disable-next-line, the other next-line escape hatch. This codebase has one, in src/components/molecules/MarqueeRow.tsx line 15, scoped to a single named rule (@next/next/no-img-element) rather than disabling lint wholesale. Name the rule you are silencing. A bare eslint-disable-next-line is the @ts-ignore of linting.

Next.js and type checking

next build type-checks every file, so a suppression in application code is still a suppression the production build respects. That matters for the other direction too: removing a stale @ts-expect-error is not optional housekeeping, because the build fails with TS2578 until you do. The repo's CI runs lint, format:check, test and build on every push, so a stale directive cannot reach main unnoticed.

Mistakes and troubleshooting

SymptomCauseFix
TS2578: Unused '@ts-expect-error' directiveThe error it covered is gone, or the directive is above the wrong lineDelete the directive, or move it to the line that actually errors
Directive added, error still reportedThe error is on a later line of a multi-line statementPut the directive directly above the offending line
ban-ts-comment on @ts-ignoreThe preset bans itUse @ts-expect-error with a description
ban-ts-comment on @ts-expect-errorNo description, or under 3 charactersAdd a real reason after the directive
Directive in JSX shows as literal text on the page// is not a comment inside JSX childrenUse {/* @ts-expect-error — reason */}
Suppressed error resurfaces as a runtime crashThe directive hid a real type error rather than a deliberate violationFix the type, or narrow with a cast and a guard

Frequently asked questions

What is the difference between @ts-ignore and @ts-expect-error? Both suppress the compiler error on the next line. @ts-expect-error additionally raises TS2578 when that line has no error, so the comment cannot outlive the problem it covered.

Can I ignore a whole file? // @ts-nocheck at the top of a file disables checking for the file. It is rarely right in a project that runs next build, since the check you are skipping is the one that protects production.

Why does the directive not silence an error two lines down? It attaches to the next physical line only. Put it directly above the line that errors, or restructure so the violation sits on its own line.

Should I use as unknown as T instead? When you can name the type you actually have, a targeted cast such as the Partial<...> above is narrower than a double cast through unknown. A double cast tells the compiler to stop looking at that expression entirely, which is close to what @ts-ignore does.

Templates in this post

ASoc Quill is a lightweight publication template with a featured-post hero, a stories feed and Minimal, Grid and Slider layouts — a content-driven site where typed post metadata earns its keep. ASoc Rally markets an AI CRM with a pipeline-overview hero and a three-tier pricing table, the kind of billing-adjacent surface that sits next to webhook handlers like the one tested above. ASoc Rank is an SEO-audit SaaS site with a free, premium and enterprise pricing table, where plan data is exactly the shape worth keeping strictly typed.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial8 min read

A Waitlist Landing Page Is One Server Action and a Honeypot

This storefront's real waitlist form: the Resend Audience API's deprecated field a copy-pasted snippet would miss, and firing one analytics event per successful subscribe, not per render.

Read more
Tutorial9 min read

Web Accessibility Tools: Which Ones Caught This Site's Real Defects

Accessibility 100 on all 8 pages, and two real defects no scanner flagged. Four defects sorted by which tool found them — and why two were structurally invisible.

Read more
Tutorial10 min read

Is Webflow Good for Ecommerce? What the SKU Cap Actually Costs

Webflow's ecommerce plans cap items and charge a transaction fee on top. This storefront's entire commerce stack — entitlements, checkout, webhook — is 843 lines with no cap at all.

Read more