TS1003Syntax Error
Since TS 1.0

Fix TS1003: Identifier Expected

Learn why TypeScript throws TS1003 when a name is missing or malformed — dangling dots, hyphenated keys, reserved words in imports — and how to fix each one.

error TS1003: Identifier expected

What This Error Means

TS1003 means the TypeScript parser reached a spot in your code where the grammar allows exactly one thing — an identifier, a plain name like orderTotal or fetchOrders — and found something else. A digit, a piece of punctuation, a reserved word, or the end of the line.

This is a parse error, not a type error. It happens in the very first pass over your source, before TypeScript knows anything about your types, so no compiler flag changes the outcome: strict, target, module and skipLibCheck are all irrelevant here. The message has been worded the same way since TypeScript 1.0.

// The general shape of the error:
const displayName = user.
//                       ~ Error: Identifier expected.

The reported position is where the parser gave up, which is usually one token after the mistake. And because the parser is now out of step with your code, a TS1003 almost always drags companions along with it — ';' expected (TS1005), Declaration or statement expected (TS1128), sometimes Expression expected (TS1109). Fix the first TS1003 and re-run before you read the rest of the list.

Common Causes

1. A Member Access Left Dangling

The most common source by far: you typed a dot, your editor popped up its completion list, and you saved or moved away before picking anything. The parser needs a property name after . and there isn't one.

// ❌ Broken
const user = { name: "Dana", city: "Berlin" }
 
const displayName = user.
//                       ~ Error: Identifier expected.

Optional chaining behaves identically — ?. is just as hungry for a name as . is:

// ❌ Broken
declare const session: { user?: { city: string } }
 
const city = session.user?.   // Error: Identifier expected.
// ✅ Fixed — finish the access
const user = { name: "Dana", city: "Berlin" }
 
const displayName = user.name

The same applies to a half-typed enum member (Status. with nothing after it) and to a chain broken across lines, where the property ended up on the wrong side of a comment.

2. A Reserved Word or default in an Import Specifier

Everything between the braces of an import is a binding name, and reserved words cannot be binding names. import { default } is the classic version of this — people reach for it when they know a module has a default export but they are already in named-import syntax.

// ❌ Broken
import { default } from "./logger"
//       ~~~~~~~ Error: Identifier expected.
// ✅ Fixed — default imports do not use braces
import logger from "./logger"
// ✅ Also fixed — keep the braces and rename with `as`
import { default as logger } from "./logger"

Any other keyword hits the same wall. import { class } from "./tokens" fails for exactly the same reason; the module may well export a member literally called class, but you have to give it a legal local name with as:

// ✅ Fixed — rename the keyword to a usable binding
import { class as tokenClass } from "./tokens"

3. A Hyphenated Key Read With Dot Notation

Property names that come from outside TypeScript — CSS module class names, vendor-prefixed CSS properties, HTTP header keys, JSON from an API — routinely contain hyphens. A hyphen is not a name character; in dot position it is the subtraction operator, so the parser wants a name on the other side of it.

// ❌ Broken
declare const banner: HTMLElement
 
banner.style.-webkit-transform = "translateZ(0)"
//           ~ Error: Identifier expected.
// ✅ Fixed — go through the string-keyed API
declare const banner: HTMLElement
 
banner.style.setProperty("-webkit-transform", "translateZ(0)")
banner.style.webkitTransform = "translateZ(0)"

For an object you own, bracket notation with a string key is the general answer, and it is what you want for CSS modules:

// ✅ Fixed — bracket notation accepts any string key
declare const styles: Record<string, string>
 
const cardClass = styles["user-card"]

Be aware that a hyphen in the middle of a dot access — styles.user-card — parses successfully as styles.user - card and reports different errors (TS2362 about the arithmetic operand and TS2304 for the undefined name card). Same root cause, same fix; only the leading-hyphen form reaches TS1003.

4. An Identifier That Starts With a Digit

Names must start with a letter, _ or $. When the first character is a digit the parser reads a numeric literal, then finds itself needing a name it never got.

// ❌ Broken
function 2faSetup(userId: string) {
  return { userId, enabled: true }
}
// Error: Identifier expected. (plus TS1351 and two TS1005s from the fallout)
// ✅ Fixed — spell the number out
function twoFactorSetup(userId: string) {
  return { userId, enabled: true }
}

This shows up most often when a name is generated from data — a plan tier, an API version, a metric key. If the string has to stay as-is, it belongs in a property, not in an identifier: const setup = { "2fa": twoFactorSetup }.

How to Fix It

  1. Go to the exact line and column, then look one token to the left. TS1003 is reported where the parser stopped, and the offending character is nearly always the token just before it — the trailing ., the -, the digit, the keyword.

  2. Fix only the first TS1003 and recompile. A single missing name knocks the parser out of sync, and everything reported after it is guesswork. It is normal for one bad character to produce five errors and for four of them to vanish on their own.

  3. Reach for bracket notation whenever the key is not a legal identifier. headers["content-type"], styles["user-card"], payload["2fa"]. Anything with a hyphen, a space, or a leading digit has to be a string. For CSS modules specifically, configuring your bundler to emit camelCase class names removes the whole class of problem.

  4. Use as in import and export specifiers for anything keyword-shaped. import { default as logger }, export { parse as default }. Do not try to work around it by renaming the module's export or by dropping to require — those are bigger changes than the one-word fix.

  5. Don't try to silence it. @ts-ignore and @ts-expect-error are type-checker directives; they have no effect on a parse error, and neither does relaxing tsconfig.json. TS1003 means the file cannot be read as TypeScript at all, so there is nothing to suppress — the code has to change.

  6. If the error points into node_modules, upgrade TypeScript. A TS1003 inside a .d.ts file you didn't write almost always means the library ships syntax your compiler version predates. skipLibCheck will not help — it skips type checking, not parsing. Bumping typescript to a version at or above the library's requirement is the fix.

FAQ

What causes TypeScript error TS1003?

TS1003 is a pure parser error: at a position where the grammar requires an identifier, the scanner produced something that can never be one. The four places it happens are after . or ?., inside an import or export specifier, in a declaration name, and after an operator that expects a named operand.

Because it fires before type checking, no compiler option influences it — you will get the same TS1003 with strict on or off, on any target, in any module system. The message text has not changed since TypeScript 1.0, so answers you find for old versions still apply.

How do I access a CSS module class with a hyphen in TypeScript?

Use a string key: styles["user-card"]. A hyphen cannot appear in a property name written with dot notation, because there the parser reads it as the subtraction operator.

declare const styles: Record<string, string>
 
const cardClass = styles["user-card"]

The better long-term fix is to stop generating hyphenated names in the first place. Most CSS module setups can expose class names in camelCase — with that on, styles.userCard works and your JSX stays readable.

How do I import the default export using named-import syntax?

You cannot import it as a bare default, because default is a reserved word and an import specifier is a binding name. Either drop the braces, which is the idiomatic form, or rename it inside them:

import logger from "./logger"
import { default as logger2 } from "./logger"

Both compile to the same thing. The as form is worth knowing for re-exports, where export { formatDate as default } is the only way to promote an existing named export to be the module's default.

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