evanw/esbuild · error · LexerPanic

Unexpected "interface"

Error message

Unexpected "interface"

What it means

In TypeScript, 'interface' is rejected when it appears after a newline in an export (non-default) statement context (js_parser.go:8335). The guard fires when opts.isExport is set, a newline precedes 'interface', and it is not an export-default form.

Source

Thrown at internal/js_parser/js_parser.go:8337

						if !p.lexer.HasNewlineBefore && (opts.isModuleScope || opts.isNamespaceScope) && (p.lexer.Token == js_lexer.TIdentifier ||
							(p.lexer.Token == js_lexer.TStringLiteral && opts.isTypeScriptDeclare)) {
							return p.parseTypeScriptNamespaceStmt(loc, opts)
						}

					case "interface":
						// "interface Foo {}"
						// "export default interface Foo {}"
						// "export default interface \n Foo {}"
						if !p.lexer.HasNewlineBefore || opts.isExportDefault {
							p.skipTypeScriptInterfaceStmt(parseStmtOpts{isModuleScope: opts.isModuleScope})
							return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
						}

						// "interface \n Foo {}"
						// "export interface \n Foo {}"
						if opts.isExport {
							p.log.AddError(&p.tracker, nameRange, "Unexpected \"interface\"")
							panic(js_lexer.LexerPanic{})
						}

					case "abstract":
						if !p.lexer.HasNewlineBefore && p.lexer.Token == js_lexer.TClass {
							return p.parseClassStmt(loc, opts)
						}

					case "global":
						// "declare module 'fs' { global { namespace NodeJS {} } }"
						if opts.isNamespaceScope && opts.isTypeScriptDeclare && p.lexer.Token == js_lexer.TOpenBrace {
							p.lexer.Next()
							p.parseStmtsUpTo(js_lexer.TCloseBrace, opts)
							p.lexer.Next()
							return js_ast.Stmt{Loc: loc, Data: js_ast.STypeScriptShared}
						}

					case "declare":
						if !p.lexer.HasNewlineBefore {

View on GitHub (pinned to f6058f8364)

Solutions

  1. Keep 'export interface' on the same line.
  2. If a default export of an interface is truly intended, use 'export default interface'.
  3. Remove the line break between 'export' and 'interface'.

Example fix

// before
export
interface Foo {}
// after
export interface Foo {}
Defensive patterns

Strategy: validation

Validate before calling

import "github.com/evanw/esbuild/pkg/api"
res := api.Transform(src, api.TransformOptions{Loader: api.LoaderTS})
for _, m := range res.Errors {
  if strings.Contains(m.Text, `Unexpected "interface"`) {
    // 'interface' appeared after a newline in an export statement
  }
}

Type guard

// Flag 'export\ninterface' (line break between export and interface).
var reExportNewlineInterface = regexp.MustCompile(`(?m)^export\s*\\n\s*interface\\b`)
func exportNewlineBeforeInterface(src string) bool { return reExportNewlineInterface.MatchString(src) }

Prevention

When it happens

Trigger: 'export\ninterface Foo {}' — a line break between 'export' and 'interface' in a named export.

Common situations: Formatter/prettier wrapping the 'export' keyword onto its own line; refactor artifacts; copy-paste of declaration + export.

Related errors


AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09). Data as JSON: /api/errors/ed9506ac0fafd502. Report an issue: GitHub.