Skip to content

Commit

Permalink
parse const type parameters from TypeScript 5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 27, 2023
1 parent 3ada8f0 commit 73523d9
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 29 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## Unreleased

* Parse `const` type parameters from TypeScript 5.0

The TypeScript 5.0 beta announcement adds [`const` type parameters](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#const-type-parameters) to the language. You can now add the `const` modifier on a type parameter of a function, method, or class like this:

```ts
type HasNames = { names: readonly string[] };
const getNamesExactly = <const T extends HasNames>(arg: T): T["names"] => arg.names;
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });
```

The type of `names` in the above example is `readonly ["Alice", "Bob", "Eve"]`. Marking the type parameter as `const` behaves as if you had written `as const` at every use instead. The above code is equivalent to the following TypeScript, which was the only option before TypeScript 5.0:

```ts
type HasNames = { names: readonly string[] };
const getNamesExactly = <T extends HasNames>(arg: T): T["names"] => arg.names;
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] } as const);
```

You can read [the announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#const-type-parameters) for more information.

* Make parsing generic `async` arrow functions more strict in `.tsx` files

Previously esbuild's TypeScript parser incorrectly accepted the following code as valid:
Expand Down
37 changes: 26 additions & 11 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2155,7 +2155,9 @@ func (p *parser) parseProperty(startLoc logger.Loc, kind js_ast.PropertyKind, op

// "class X { foo?<T>(): T }"
// "const x = { foo<T>(): T {} }"
hasTypeParameters = !hasDefiniteAssignmentAssertionOperator && p.skipTypeScriptTypeParameters(typeParametersNormal)
if !hasDefiniteAssignmentAssertionOperator {
hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier) != didNotSkipAnything
}
}

// Parse a class field with an optional initial value
Expand Down Expand Up @@ -2716,9 +2718,14 @@ func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L, f
// "async<T>()"
// "async <T>() => {}"
case js_lexer.TLessThan:
if p.options.ts.Parse && (!p.options.jsx.Parse || p.isTSArrowFnJSX()) && p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() {
p.lexer.Next()
return p.parseParenExpr(asyncRange.Loc, level, parenExprOpts{asyncRange: asyncRange})
if p.options.ts.Parse && (!p.options.jsx.Parse || p.isTSArrowFnJSX()) {
if result := p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(); result != didNotSkipAnything {
p.lexer.Next()
return p.parseParenExpr(asyncRange.Loc, level, parenExprOpts{
asyncRange: asyncRange,
forceArrowFn: result == definitelyTypeParameters,
})
}
}
}
}
Expand Down Expand Up @@ -2761,7 +2768,7 @@ func (p *parser) parseFnExpr(loc logger.Loc, isAsync bool, asyncRange logger.Ran

// Even anonymous functions can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptTypeParameters(allowConstModifier)
}

await := allowIdent
Expand Down Expand Up @@ -3437,7 +3444,7 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF

// Even anonymous classes can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations)
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier)
}

class := p.parseClass(classKeyword, name, parseClassOpts{})
Expand Down Expand Up @@ -3628,23 +3635,29 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
// <A>(x)
// <[]>(x)
// <A[]>(x)
// <const>(x)
//
// An arrow function with type parameters:
// <A>(x) => {}
// <A, B>(x) => {}
// <A = B>(x) => {}
// <A extends B>(x) => {}
// <const A>(x) => {}
// <const A extends B>(x) => {}
//
// TSX:
//
// A JSX element:
// <A>(x) => {}</A>
// <A extends>(x) => {}</A>
// <A extends={false}>(x) => {}</A>
// <const A extends>(x) => {}</const>
//
// An arrow function with type parameters:
// <A, B>(x) => {}
// <A extends B>(x) => {}
// <const>(x)</const>
// <const A extends B>(x) => {}
//
// A syntax error:
// <[]>(x)
Expand All @@ -3653,7 +3666,7 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
// <A = B>(x) => {}

if p.options.ts.Parse && p.options.jsx.Parse && p.isTSArrowFnJSX() {
p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptTypeParameters(allowConstModifier)
p.lexer.Expect(js_lexer.TOpenParen)
return p.parseParenExpr(loc, level, parenExprOpts{forceArrowFn: true})
}
Expand Down Expand Up @@ -3700,9 +3713,11 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF

// "<T>(x)"
// "<T>(x) => {}"
if p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() {
if result := p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(); result != didNotSkipAnything {
p.lexer.Expect(js_lexer.TOpenParen)
return p.parseParenExpr(loc, level, parenExprOpts{})
return p.parseParenExpr(loc, level, parenExprOpts{
forceArrowFn: result == definitelyTypeParameters,
})
}

// "<T>x"
Expand Down Expand Up @@ -5811,7 +5826,7 @@ func (p *parser) parseClassStmt(loc logger.Loc, opts parseStmtOpts) js_ast.Stmt

// Even anonymous classes can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations)
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier)
}

classOpts := parseClassOpts{
Expand Down Expand Up @@ -6089,7 +6104,7 @@ func (p *parser) parseFnStmt(loc logger.Loc, opts parseStmtOpts, isAsync bool, a

// Even anonymous functions can have TypeScript type parameters
if p.options.ts.Parse {
p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptTypeParameters(allowConstModifier)
}

// Introduce a fake block scope for function declarations inside if statements
Expand Down
67 changes: 50 additions & 17 deletions internal/js_parser/ts_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,12 @@ loop:
return
}

p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptTypeParameters(allowConstModifier)
p.skipTypeScriptParenOrFnType()

case js_lexer.TLessThan:
// "<T>() => Foo<T>"
p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptTypeParameters(allowConstModifier)
p.skipTypeScriptParenOrFnType()

case js_lexer.TOpenParen:
Expand Down Expand Up @@ -594,7 +594,7 @@ func (p *parser) skipTypeScriptObjectType() {
}

// Type parameters come right after the optional mark
p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptTypeParameters(0)

switch p.lexer.Token {
case js_lexer.TColon:
Expand Down Expand Up @@ -635,32 +635,59 @@ func (p *parser) skipTypeScriptObjectType() {
p.lexer.Expect(js_lexer.TCloseBrace)
}

type typeParameters uint8
type typeParameterFlags uint8

const (
typeParametersNormal typeParameters = iota
typeParametersWithInOutVarianceAnnotations
// TypeScript 4.7
allowInOutVarianceAnnotations typeParameterFlags = 1 << iota

// TypeScript 5.0
allowConstModifier
)

type skipTypeScriptTypeParametersResult uint8

const (
didNotSkipAnything skipTypeScriptTypeParametersResult = iota
couldBeTypeCast
definitelyTypeParameters
)

// This is the type parameter declarations that go with other symbol
// declarations (class, function, type, etc.)
func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool {
func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipTypeScriptTypeParametersResult {
if p.lexer.Token != js_lexer.TLessThan {
return false
return didNotSkipAnything
}

p.lexer.Next()
result := couldBeTypeCast

for {
hasIn := false
hasOut := false
expectIdentifier := true
invalidModifierRange := logger.Range{}

// Scan over a sequence of "in" and "out" modifiers (a.k.a. optional variance annotations)
// Scan over a sequence of "in" and "out" modifiers (a.k.a. optional
// variance annotations) as well as "const" modifiers
for {
if p.lexer.Token == js_lexer.TConst {
if invalidModifierRange.Len == 0 && (flags&allowConstModifier) == 0 {
// Valid:
// "class Foo<const T> {}"
// Invalid:
// "interface Foo<const T> {}"
invalidModifierRange = p.lexer.Range()
}
result = definitelyTypeParameters
p.lexer.Next()
expectIdentifier = true
continue
}

if p.lexer.Token == js_lexer.TIn {
if invalidModifierRange.Len == 0 && (mode != typeParametersWithInOutVarianceAnnotations || hasIn || hasOut) {
if invalidModifierRange.Len == 0 && ((flags&allowInOutVarianceAnnotations) == 0 || hasIn || hasOut) {
// Valid:
// "type Foo<in T> = T"
// Invalid:
Expand All @@ -676,7 +703,7 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool {

if p.lexer.IsContextualKeyword("out") {
r := p.lexer.Range()
if invalidModifierRange.Len == 0 && mode != typeParametersWithInOutVarianceAnnotations {
if invalidModifierRange.Len == 0 && (flags&allowInOutVarianceAnnotations) == 0 {
invalidModifierRange = r
}
p.lexer.Next()
Expand Down Expand Up @@ -714,12 +741,14 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool {

// "class Foo<T extends number> {}"
if p.lexer.Token == js_lexer.TExtends {
result = definitelyTypeParameters
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}

// "class Foo<T = void> {}"
if p.lexer.Token == js_lexer.TEquals {
result = definitelyTypeParameters
p.lexer.Next()
p.skipTypeScriptType(js_ast.LLowest)
}
Expand All @@ -729,12 +758,13 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool {
}
p.lexer.Next()
if p.lexer.Token == js_lexer.TGreaterThan {
result = definitelyTypeParameters
break
}
}

p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */)
return true
return result
}

func (p *parser) skipTypeScriptTypeArguments(isInsideJSXElement bool) bool {
Expand Down Expand Up @@ -787,7 +817,7 @@ func (p *parser) trySkipTypeScriptTypeArgumentsWithBacktracking() bool {
return true
}

func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() bool {
func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() skipTypeScriptTypeParametersResult {
oldLexer := p.lexer
p.lexer.IsLogDisabled = true

Expand All @@ -801,15 +831,15 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking()
}
}()

p.skipTypeScriptTypeParameters(typeParametersNormal)
result := p.skipTypeScriptTypeParameters(allowConstModifier)
if p.lexer.Token != js_lexer.TOpenParen {
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
return result
}

func (p *parser) trySkipTypeScriptArrowReturnTypeWithBacktracking() bool {
Expand Down Expand Up @@ -896,6 +926,9 @@ func (p *parser) isTSArrowFnJSX() (isTSArrowFn bool) {
p.lexer.Next()

// Look ahead to see if this should be an arrow function instead
if p.lexer.Token == js_lexer.TConst {
p.lexer.Next()
}
if p.lexer.Token == js_lexer.TIdentifier {
p.lexer.Next()
if p.lexer.Token == js_lexer.TComma || p.lexer.Token == js_lexer.TEquals {
Expand Down Expand Up @@ -1015,7 +1048,7 @@ func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) {
p.localTypeNames[name] = true
}

p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations)
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations)

if p.lexer.Token == js_lexer.TExtends {
p.lexer.Next()
Expand Down Expand Up @@ -1090,7 +1123,7 @@ func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) {
p.localTypeNames[name] = true
}

p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations)
p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations)
p.lexer.Expect(js_lexer.TEquals)
p.skipTypeScriptType(js_ast.LLowest)
p.lexer.ExpectOrInsertSemicolon()
Expand Down
Loading

0 comments on commit 73523d9

Please sign in to comment.