diff --git a/spec-compliance-tests/babel-tests/check-babel-tests.ts b/spec-compliance-tests/babel-tests/check-babel-tests.ts index 01f78d14..6ecae671 100644 --- a/spec-compliance-tests/babel-tests/check-babel-tests.ts +++ b/spec-compliance-tests/babel-tests/check-babel-tests.ts @@ -67,7 +67,6 @@ flow/scope/declare-module flow/this-annotation/function-type flow/typecasts/yield jsx/basic/3 -typescript/cast/as typescript/export/as-namespace typescript/import/export-import typescript/import/export-import-require @@ -76,7 +75,6 @@ typescript/import/export-import-type-require typescript/import/import-default-id-type typescript/import/type-asi typescript/import/type-equals-require -typescript/type-arguments/instantiation-expression-binary-operator ` .split("\n") .filter((s) => s); diff --git a/src/parser/plugins/typescript.ts b/src/parser/plugins/typescript.ts index 10950613..cf57085b 100644 --- a/src/parser/plugins/typescript.ts +++ b/src/parser/plugins/typescript.ts @@ -1242,6 +1242,9 @@ export function tsParseSubscript( // Tagged template with a type argument. parseTemplate(); } else if ( + // The remaining possible case is an instantiation expression, e.g. + // Array . Check for a few cases that would disqualify it and + // cause us to bail out. // a>c is not (a)>c, but a<(b>>c) state.type === tt.greaterThan || // ac is (ac diff --git a/src/parser/tokenizer/index.ts b/src/parser/tokenizer/index.ts index 3349a601..b02acb3d 100644 --- a/src/parser/tokenizer/index.ts +++ b/src/parser/tokenizer/index.ts @@ -536,7 +536,7 @@ function readToken_gt(): void { /** * Called after `as` expressions in TS; we're switching from a type to a - * non-type context, so a > token may actually be >= + * non-type context, so a > token may actually be >= . */ export function rescan_gt(): void { if (state.type === tt.greaterThan) { @@ -565,7 +565,12 @@ function readToken_question(): void { // '?' const nextChar = input.charCodeAt(state.pos + 1); const nextChar2 = input.charCodeAt(state.pos + 2); - if (nextChar === charCodes.questionMark && !state.isType) { + if ( + nextChar === charCodes.questionMark && + // In Flow (but not TypeScript), ??string is a valid type that should be + // tokenized as two individual ? tokens. + !(isFlowEnabled && state.isType) + ) { if (nextChar2 === charCodes.equalsTo) { // '??=' finishOp(tt.assign, 3); diff --git a/test/flow-test.ts b/test/flow-test.ts index cdad9228..b919f2d8 100644 --- a/test/flow-test.ts +++ b/test/flow-test.ts @@ -688,4 +688,15 @@ describe("transform flow", () => { `, ); }); + + it("handles two ? operators in a row", () => { + assertFlowResult( + ` + type T = ??number; + `, + `"use strict"; + + `, + ); + }); }); diff --git a/test/typescript-test.ts b/test/typescript-test.ts index a657a9ad..b220202e 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -5,6 +5,7 @@ import { IMPORT_DEFAULT_PREFIX, IMPORT_WILDCARD_PREFIX, JSX_PREFIX, + NULLISH_COALESCE_PREFIX, OPTIONAL_CHAIN_PREFIX, } from "./prefixes"; import {assertResult, devProps} from "./util"; @@ -2388,15 +2389,17 @@ describe("typescript transform", () => { ); }); - it("properly handles a >= symbol after an `as` cast", () => { + it("properly handles >= and ?? after `as`", () => { assertTypeScriptResult( ` const x: string | number = 1; if (x as number >= 5) {} + if (y as unknown ?? false) {} `, - `"use strict"; + `"use strict";${NULLISH_COALESCE_PREFIX} const x = 1; if (x >= 5) {} + if (_nullishCoalesce(y , () => ( false))) {} `, ); }); @@ -3214,6 +3217,18 @@ describe("typescript transform", () => { ); }); + it("allows instantiation expressions followed by ??", () => { + assertResult( + ` + const foo = a ?? c; + `, + `${NULLISH_COALESCE_PREFIX} + const foo = _nullishCoalesce(a, () => ( c)); + `, + {transforms: ["typescript"]}, + ); + }); + it("allows extends constraints on infer type variables", () => { assertResult( `