TS1128Syntax Error
Since TS 1.0

Fix TS1128: Declaration or Statement Expected

Learn why TypeScript throws TS1128 when a token cannot start a statement, and how to fix stray braces, detached else blocks and outdated compilers.

error TS1128: Declaration or statement expected

What This Error Means

TS1128 means the TypeScript parser was at a position where a new statement or declaration has to begin — the top level of a file, the inside of a function body, the start of a line in a block — and the next token could not possibly start one. A leftover }, a ) with nothing to close, an else with no if in front of it: none of these can open a statement, so the parser stops and reports "Declaration or statement expected".

This happens during parsing, before any type checking, so no compiler option changes the result. strict, target, module and skipLibCheck are all irrelevant here. Two things do matter: how balanced your braces are, and which TypeScript version is reading the file — syntax added in a later release parses cleanly on a new compiler and reports TS1128 on an old one.

// The general shape of the error:
function loadUserProfile(id: string) {
  return fetch(`/api/users/${id}`)
}
}
// ~ Error: Declaration or statement expected.

The important habit with TS1128 is to distrust the line number. Unlike TS1109 (Expression expected), which normally points straight at the problem, TS1128 fires at the first token the parser could not use — and with an unbalanced brace that can be many lines below the edit that caused it. Read upwards from the reported line, and fix only the first TS1128 in a file before re-running; the TS1005 and TS1003 reports underneath it are usually cascade noise that disappears on their own.

Common Causes

1. An Extra Closing Brace

The most common shape by far. A deleted if, a collapsed try, or a merge that kept both sides of a conflict leaves one more } than there are open blocks. The function closes correctly, and then a second brace appears where a new declaration should start.

// ❌ Broken
function loadUserProfile(id: string) {
  return fetch(`/api/users/${id}`)
}
}
// ~ Error: Declaration or statement expected.
// ✅ Fixed — one closing brace per opening brace
function loadUserProfile(id: string) {
  return fetch(`/api/users/${id}`)
}

Note where the compiler pointed: at line 4, not at the deleted block on line 1. Whatever removed the extra nesting is the real edit, and it is above the error. This is also the signature of a badly resolved merge conflict — if the file has <<<<<<< markers left in it you will get a whole cascade of parse errors, TS1128 among them.

2. A Stray ); Left Behind by a Refactor

Turning a callback or an IIFE into a plain function is a two-part edit: you change the head of the construct and its tail. Change only the head and the closing ) survives with nothing to close.

// ❌ Broken — was initAnalytics = (() => { … })()
export function initAnalytics(appVersion: string) {
  console.log("boot", appVersion)
});
// ~ Error: Declaration or statement expected.
// ✅ Fixed — a function declaration ends at its closing brace
export function initAnalytics(appVersion: string) {
  console.log("boot", appVersion)
}

The same thing happens in test files when a describe("…", () => { … }) becomes a top-level helper and the trailing }); stays put, and when a .then(() => { … }) is rewritten with await. Any time you delete a (, search for the ) that belonged to it.

3. An else, catch or finally Detached From Its Block

These clauses are not statements — they are parts of one. else may only follow the if branch directly, and catch/finally may only follow the try block. Insert anything in between and the parser has already finished the if statement by the time it reaches else.

// ❌ Broken — logAccess() separates the branches
if (isPaid) { unlock() }
logAccess()
else { showPaywall() }
// ~ Error: Declaration or statement expected.
// ✅ Fixed — else attaches to the if, the extra call moves out
if (isPaid) {
  unlock()
} else {
  showPaywall()
}
logAccess()

A semicolon does the same damage more quietly: if (isPaid) { unlock() }; followed by else ends the statement at the ;, and the else is then orphaned. If the branches really do need shared code around them, put that code before the if or after the whole statement — never between the two halves.

4. Newer Syntax Read by an Older TypeScript

If the code looks perfectly valid and the error still fires, the compiler reading it may predate the syntax. Type-only exports (export type { … }) arrived in TypeScript 3.8, accessor and satisfies in 4.9, using declarations in 5.2. Of these it is the type-only export that reports TS1128 — an older parser cannot start a statement with it. satisfies and using are read as plain identifiers by a compiler that predates them, so the same version skew surfaces as TS1005 or TS1109 instead. The diagnosis is the same either way: the compiler is older than the code.

// ❌ Broken — only on TypeScript below 3.8
export type { UserProfile } from "./types"
// ~ Error: Declaration or statement expected.
# ✅ Fixed — upgrade the compiler that reports the error
npm install --save-dev typescript@latest
# VS Code: "TypeScript: Select TypeScript Version" → Use Workspace Version

This is also why TS1128 turns up inside node_modules. A library's .d.ts may use syntax newer than the TypeScript your project pins, and skipLibCheck does not help: it skips type checking of declaration files, not parsing them. Either upgrade TypeScript or pin the library to a release that predates the syntax.

How to Fix It

  1. Read upwards from the reported line. TS1128 marks the first token the parser could not use, and with an unbalanced brace the real edit is above it. Put the cursor on the closing brace the error points at and use your editor's Go to Bracket command — if it jumps somewhere you did not expect, you have found the mistake.

  2. Balance the file with a formatter. Run Prettier or your editor's format-on-save over the file. A formatter cannot format code it cannot parse, and the position it refuses at is usually much closer to the actual unbalanced brace or parenthesis than the compiler's report. Reformatting also makes the indentation reveal a block that closed one level too early.

  3. Check the tail of every construct you changed. Converting an IIFE, a callback or a .then() chain into a plain function means deleting a ) as well as the arrow. Removing an if or a try means removing its } too. Search the lines below your edit for the orphaned closer before you re-run the compiler.

  4. Compare compiler versions before you rewrite anything. Run npx tsc -v in the project and compare it to your editor's version (VS Code: TypeScript: Select TypeScript Version) and to whatever ts-jest, ts-node or CI resolves — npm ls typescript often shows more than one. If the error appears in only one of them, the syntax is fine and the parser is old. Upgrade it rather than rewriting valid code, and never delete a type-only export just to please a stale compiler.

  5. Put JSX in .tsx files, not .ts. In a .ts file, <section> is parsed as a type assertion rather than as markup, and the rest of the component becomes a cascade of TS1005, TS1161 and TS1128. Rename the file and set "jsx": "react-jsx" (or "preserve" when a bundler such as Next.js handles the transform) in tsconfig.json.

  6. Keep one TypeScript version and let your editor use it. A single typescript entry in devDependencies, the workspace version selected in the editor, and the same version in CI removes the entire "it only fails in one place" category of TS1128 — and makes every remaining report an honest brace problem you can fix in seconds.

FAQ

What causes TypeScript error TS1128?

TS1128 comes from the parser, before type checking starts. TypeScript's grammar has positions where a statement or a declaration must begin: the top level of a module, the body of a function, each new line inside a block. When the next token cannot begin one, you get "Declaration or statement expected".

In practice that token is nearly always an extra } or ) left by a refactor or a merge, or an else, catch or finally that was separated from the block it belongs to. The third cause is less obvious: perfectly valid syntax that the compiler reading the file is too old to parse.

How do I find the unbalanced brace that causes Declaration or statement expected?

Read upwards, not downwards. The error is reported at the first token the parser could not consume, which with a brace problem is often well below the edit that caused it:

// the mistake: this block lost its `if (…)` header
{
  unlock()
}
// …many lines later…
}
// ~ Error: Declaration or statement expected.

Click the brace the error points at and use Go to Bracket — if it matches something other than what you expect, you have the culprit. Running Prettier is the quickest second opinion, because it refuses to format an unparseable file and names the position it gave up at.

Why do I get TS1128 inside node_modules and .d.ts files?

Because skipLibCheck does not do what the name suggests. It skips type checking of declaration files; every .d.ts your program pulls in still has to be parsed. A library that ships export type { … } is unreadable to a compiler that predates it, and the parse error surfaces as TS1128 with a path inside node_modules. Other syntax newer than your compiler — satisfies, using — breaks the same file the same way, but reports TS1005 or TS1109 rather than TS1128.

The fix is a version alignment, not a code change. Compare the TypeScript version the library documents with npx tsc -v, and check npm ls typescript for a second copy hoisted by Angular CLI or ts-jest. Upgrade TypeScript where you can; where you cannot, pin the library to the last release that parsed on your compiler.

Related Errors

Practice This

Browse all TypeScript practice challenges to keep sharpening your type-level skills.

Share this reference

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges