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
- Keep 'export interface' on the same line.
- If a default export of an interface is truly intended, use 'export default interface'.
- 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
- Keep 'export interface' on the same line.
- Run tsc on .ts files to catch malformed export declarations.
- Review formatter settings that wrap 'export' onto its own line.
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
- Unexpected newline after "type"
- Unexpected newline after "async"
- Unexpected newline before "=>"
- Unexpected ":"
- For loop initializers cannot start with "async of"
AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09).
Data as JSON: /api/errors/ed9506ac0fafd502.
Report an issue: GitHub.