diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a416a045c..4e46cfc8bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +* Fix parsing of TypeScript `infer` inside a conditional `extends` ([#2675](https://github.com/evanw/esbuild/issues/2675)) + + Unlike JavaScript, parsing TypeScript sometimes requires backtracking. The `infer A` type operator can take an optional constraint of the form `infer A extends B`. However, this syntax conflicts with the similar conditional type operator `A extends B ? C : D` in cases where the syntax is combined, such as `infer A extends B ? C : D`. This is supposed to be parsed as `(infer A) extends B ? C : D`. Previously esbuild incorrectly parsed this as `(infer A extends B) ? C : D` instead, which is a parse error since the `?:` conditional operator requires the `extends` keyword as part of the conditional type. TypeScript disambiguates by speculatively parsing the `extends` after the `infer`, but backtracking if a `?` token is encountered afterward. With this release, esbuild should now do the same thing, so esbuild should now correctly parse these types. Here's a real-world example of such a type: + + ```ts + type Normalized = T extends Array + ? Dictionary> + : { + [P in keyof T]: T[P] extends Array + ? Dictionary> + : Normalized + } + ``` + * Avoid unnecessary watch mode rebuilds when debug logging is enabled ([#2661](https://github.com/evanw/esbuild/issues/2661)) When debug-level logs are enabled (such as with `--log-level=debug`), esbuild's path resolution subsystem generates debug log messages that say something like "Read 20 entries for directory /home/user" to help you debug what esbuild's path resolution is doing. This caused esbuild's watch mode subsystem to add a dependency on the full list of entries in that directory since if that changes, the generated log message would also have to be updated. However, meant that on systems where a parent directory undergoes constant directory entry churn, esbuild's watch mode would continue to rebuild if `--log-level=debug` was passed. diff --git a/internal/js_parser/ts_parser.go b/internal/js_parser/ts_parser.go index b980755ae98..081312976cd 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -316,11 +316,11 @@ loop: // "type Foo = Bar extends [infer T] ? T : null" // "type Foo = Bar extends [infer T extends string] ? T : null" + // "type Foo = Bar extends [infer T extends string ? infer T : never] ? T : null" if p.lexer.Token != js_lexer.TColon || (!opts.isIndexSignature && !opts.allowTupleLabels) { p.lexer.Expect(js_lexer.TIdentifier) if p.lexer.Token == js_lexer.TExtends { - p.lexer.Next() - p.skipTypeScriptType(js_ast.LPrefix) + p.trySkipTypeScriptConstraintOfInferTypeWithBacktracking() } } break loop @@ -853,6 +853,32 @@ func (p *parser) trySkipTypeScriptArrowArgsWithBacktracking() bool { return true } +func (p *parser) trySkipTypeScriptConstraintOfInferTypeWithBacktracking() bool { + oldLexer := p.lexer + p.lexer.IsLogDisabled = true + + // Implement backtracking by restoring the lexer's memory to its original state + defer func() { + r := recover() + if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic { + p.lexer = oldLexer + } else if r != nil { + panic(r) + } + }() + + p.lexer.Expect(js_lexer.TExtends) + p.skipTypeScriptType(js_ast.LPrefix) + if p.lexer.Token == js_lexer.TQuestion { + p.lexer.Unexpected() + } + + // Restore the log disabled flag. Note that we can't just set it back to false + // because it may have been true to start with. + p.lexer.IsLogDisabled = oldLexer.IsLogDisabled + return true +} + // Returns true if the current less-than token is considered to be an arrow // function under TypeScript's rules for files containing JSX syntax func (p *parser) isTSArrowFnJSX() (isTSArrowFn bool) { diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index 1dc89d250b7..8a1f34fa889 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -180,6 +180,7 @@ func TestTSTypes(t *testing.T) { expectPrintedTS(t, "type Foo = Bar extends [infer T] ? T : null", "") expectPrintedTS(t, "type Foo = Bar extends [infer T extends string] ? T : null", "") expectPrintedTS(t, "let x: A extends B ? D : never", "let x;\n") + expectPrintedTS(t, "let x: A extends B ? D : never", "let x;\n") expectPrintedTS(t, "let x: A.B", "let x;\n") expectPrintedTS(t, "let x: A.B=2", "let x = 2;\n")