Repository: prettier/prettier (JavaScript)
Date: 2022-04-02 18:06:41+00:00
Task type: Bug Fix

Issue:
"as" TypeScript keyword yields error within Vue template
<!--

BEFORE SUBMITTING AN ISSUE:

1.  Search for your issue on GitHub: https://github.com/prettier/prettier/issues
    A large number of opened issues are duplicates of existing issues.
    If someone has already opened an issue for what you are experiencing,
    you do not need to open a new issue — please add a 👍 reaction to the
    existing issue instead.

2.  We get a lot of requests for adding options, but Prettier is
    built on the principle of being opinionated about code formatting.
    This means we add options only in the case of strict technical necessity.
    Find out more: https://prettier.io/docs/en/option-philosophy.html

Don't fill the form below manually! Let a program create a report for you:

1. Go to https://prettier.io/playground
2. Paste your code and set options
3. Press the "Report issue" button in the lower right

-->

**Prettier 2.5.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAeeBbADgGwIbwB8AOlKlgAQBuAtAJYBmAvMSHQM4BiEOAJnACcAFAzo44ASlaFgwCiLFwKedhQwBPDBF4BXcQDpufQRP388vHHShKAvrdQB6LIQpv3pJ5lwE4JKJ7sYAJ0WDAU+FAA5iwgMOzSno5BIWH+gcGh4exwMDqUkTGs8YlkyZlppIEw6uIUQRBYcLwReNGxQewJIP5O7DXihCAANCCNMHTQ7MigeAICEADuAApzCNMoeDiLeOrTowBGAnhgANa5AMp4GHAAMtZwyAxbOYfHZ5dYJ9ZRyDACOjgozgGAOzX4vFubSiOjwUTg3AEGAIE2iyBAeB0MAgIxAAAsYBgcAB1PF0eDsL5gOAXdbkuhUcnqdFgLq46w5AQwZbHKLIp4vIEgABW7AAHhcfuIAIo6CDwAU4V4gL4CTnoqiA3FYEKwYl0XgwPHIACMACYAAyjHUQHLE45YdE6uCcqiPUYARzl8B5jQ2GPYNBszWauIEcC9dHDPLh-KQzyVQpyGDofwBSalcFl8seSH+gNGMDwB31huNSAALIXjmIfgBhCAYOMgF0AVlxOhyABVixsE8rNXAAJJQfiwC4VGAAQVHFwGuf7cHsQA)

```sh
# Options:
--parser vue
--embedded-language-formatting auto
--single-quote
--tab-width 4
--print-width 120
```

**Input:**

```vue
<template>
<p v-if="isFolder(file)">{{ (file as mymodule.Folder).deadline }}</p>      
</template>

<script lang="ts">
</script>

<script setup lang="ts">
</script>

<style scoped lang="scss">
</style>
```

**Output:**

```vue
SyntaxError: Unexpected token, expected "," (1:8)
> 1 |  (file as mymodule.Folder).deadline 
    |        ^
```

**Expected behavior:**
`prettier` should recognise TypeScript patterns such as the `as` keyword within Vue templates. I believe this issue has to do with the `--embedded-language-formatting` system not being able to parse the keyword, as when this option is set off the error is not given; however, setting this option off is not a suitable solution as it is quite useful.


Issue discussion:
Any reference that this is allowed in Vue SFC?
@fisker I don't know about any Vue doc covering this specific use case, so official support on this is unknown to me. However, AFAIK Vue 3 fully supports TypeScript and the `as` keyword is part of the language. Furthermore, my app runs just fine with a bunch of `as` keywords in the templates, including an identical example to the one given above (which is not the same as Vue officially supporting it, which is what you are asking, but still).

For more context, prettier not only throws an error with the `{{ }}` syntax, but also with things like:

```vue
<p v-if="isFolder(file as mymodule.Folder)">test</p>
```
@fisker never mind, it is officially supported. [Here](https://vuejs.org/guide/typescript/overview.html#typescript-in-templates) you have an example in the official docs.
Thank you.
To add a bit more context: using the `as` keyword within the `<script>` section does work fine, so the bug is only happening with embedded TS within templates.
> We need to update the vue expression parser to allow typescript syntax, luckily we have babel-ts, so we don't need use typescript parser.

Original posted https://github.com/prettier/plugin-pug/issues/365#issuecomment-1085622997

That's a related issue from plugin.

Solution patch:
diff --git a/changelog_unreleased/vue/12584.md b/changelog_unreleased/vue/12584.md
new file mode 100644
index 000000000000..1bada237fd3f
--- /dev/null
+++ b/changelog_unreleased/vue/12584.md
@@ -0,0 +1,31 @@
+#### Allow formatting for Vue template expression written in TypeScript (#12584 by @sosukesuzuki)
+
+<!-- prettier-ignore -->
+```vue
+<!-- input -->
+<script setup lang="ts">
+let x: string | number = 1
+</script>
+
+<template>
+  {{ (x      as      number).toFixed(2) }}
+</template>
+
+<!-- Prettier stable -->
+<script setup lang="ts">
+let x: string | number = 1
+</script>
+
+<template>
+  {{ (x      as      number).toFixed(2) }}
+</template>
+
+<!-- Prettier main -->
+<script setup lang="ts">
+let x: string | number = 1;
+</script>
+
+<template>
+  {{ (x as number).toFixed(2) }}
+</template>
+```
diff --git a/src/language-html/embed.js b/src/language-html/embed.js
index ef83e63649a8..44fbba81d5e0 100644
--- a/src/language-html/embed.js
+++ b/src/language-html/embed.js
@@ -127,11 +127,14 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
 
     if (isKeyMatched(vueEventBindingPatterns)) {
       const value = getValue();
+      const parser = isVueEventBindingExpression(value)
+        ? "__js_expression"
+        : options.__should_parse_vue_template_with_ts
+        ? "__vue_ts_event_binding"
+        : "__vue_event_binding";
       return printMaybeHug(
         attributeTextToDoc(value, {
-          parser: isVueEventBindingExpression(value)
-            ? "__js_expression"
-            : "__vue_event_binding",
+          parser,
         })
       );
     }
@@ -318,7 +321,9 @@ function embed(path, print, textToDoc, options) {
           textToDocOptions.parser = "__ng_interpolation";
           textToDocOptions.trailingComma = "none";
         } else if (options.parser === "vue") {
-          textToDocOptions.parser = "__vue_expression";
+          textToDocOptions.parser = options.__should_parse_vue_template_with_ts
+            ? "__vue_ts_expression"
+            : "__vue_expression";
         } else {
           textToDocOptions.parser = "__js_expression";
         }
diff --git a/src/language-html/print-preprocess.js b/src/language-html/print-preprocess.js
index 0add77b63d39..789ab7e565dd 100644
--- a/src/language-html/print-preprocess.js
+++ b/src/language-html/print-preprocess.js
@@ -14,6 +14,7 @@ const {
   isLeadingSpaceSensitiveNode,
   isTrailingSpaceSensitiveNode,
   isWhitespaceSensitiveNode,
+  isVueScriptTag,
 } = require("./utils/index.js");
 
 const PREPROCESS_PIPELINE = [
@@ -27,6 +28,7 @@ const PREPROCESS_PIPELINE = [
   addHasHtmComponentClosingTag,
   addIsSpaceSensitive,
   mergeSimpleElementIntoText,
+  markTsScript,
 ];
 
 function preprocess(ast, options) {
@@ -408,4 +410,19 @@ function addIsSpaceSensitive(ast, options) {
   });
 }
 
+function markTsScript(ast, options) {
+  if (options.parser === "vue") {
+    const vueScriptTag = ast.children.find((child) =>
+      isVueScriptTag(child, options)
+    );
+    if (!vueScriptTag) {
+      return;
+    }
+    const { lang } = vueScriptTag.attrMap;
+    if (lang === "ts" || lang === "typescript") {
+      options.__should_parse_vue_template_with_ts = true;
+    }
+  }
+}
+
 module.exports = preprocess;
diff --git a/src/language-html/utils/index.js b/src/language-html/utils/index.js
index 18b46cedf6ad..e3c962e32b6a 100644
--- a/src/language-html/utils/index.js
+++ b/src/language-html/utils/index.js
@@ -638,6 +638,10 @@ function getTextValueParts(node, value = node.value) {
     : getDocParts(join(line, splitByHtmlWhitespace(value)));
 }
 
+function isVueScriptTag(node, options) {
+  return isVueSfcBlock(node, options) && node.name === "script";
+}
+
 module.exports = {
   htmlTrim,
   htmlTrimPreserveIndentation,
@@ -657,6 +661,7 @@ module.exports = {
   inferScriptParser,
   isVueCustomBlock,
   isVueNonHtmlBlock,
+  isVueScriptTag,
   isVueSlotAttribute,
   isVueSfcBindingsAttribute,
   isDanglingSpaceSensitiveNode,
diff --git a/src/language-js/parse/babel.js b/src/language-js/parse/babel.js
index b6a290d6b1c0..5f79257ac741 100644
--- a/src/language-js/parse/babel.js
+++ b/src/language-js/parse/babel.js
@@ -170,6 +170,11 @@ const parseEstree = createParse(
 );
 const parseExpression = createParse("parseExpression", appendPlugins(["jsx"]));
 
+const parseTSExpression = createParse(
+  "parseExpression",
+  appendPlugins(["typescript"])
+);
+
 // Error codes are defined in
 //  - https://github.com/babel/babel/blob/v7.14.0/packages/babel-parser/src/parser/error-message.js
 //  - https://github.com/babel/babel/blob/v7.14.0/packages/babel-parser/src/plugins/typescript/index.js#L69-L153
@@ -228,21 +233,27 @@ const allowedMessageCodes = new Set([
 ]);
 
 const babel = createParser(parse);
+const babelTs = createParser(parseTypeScript);
 const babelExpression = createParser(parseExpression);
+const babelTSExpression = createParser(parseTSExpression);
 
 // Export as a plugin so we can reuse the same bundle for UMD loading
 module.exports = {
   parsers: {
     babel,
     "babel-flow": createParser(parseFlow),
-    "babel-ts": createParser(parseTypeScript),
+    "babel-ts": babelTs,
     ...jsonParsers,
     /** @internal */
     __js_expression: babelExpression,
     /** for vue filter */
     __vue_expression: babelExpression,
+    /** for vue filter written in TS */
+    __vue_ts_expression: babelTSExpression,
     /** for vue event binding to handle semicolon */
     __vue_event_binding: babel,
+    /** for vue event binding written in TS to handle semicolon */
+    __vue_ts_event_binding: babelTs,
     /** verify that we can print this AST */
     __babel_estree: createParser(parseEstree),
   },
diff --git a/src/language-js/parse/parsers.js b/src/language-js/parse/parsers.js
index eef5cddeb50d..5878696cfa87 100644
--- a/src/language-js/parse/parsers.js
+++ b/src/language-js/parse/parsers.js
@@ -26,9 +26,15 @@ module.exports = {
   get __vue_expression() {
     return require("./babel.js").parsers.__vue_expression;
   },
+  get __vue_ts_expression() {
+    return require("./babel.js").parsers.__vue_ts_expression;
+  },
   get __vue_event_binding() {
     return require("./babel.js").parsers.__vue_event_binding;
   },
+  get __vue_ts_event_binding() {
+    return require("./babel.js").parsers.__vue_ts_event_binding;
+  },
   // JS - Flow
   get flow() {
     return require("./flow.js").parsers.flow;
diff --git a/src/language-js/print-preprocess.js b/src/language-js/print-preprocess.js
index b776cab9bc53..190dc68e54eb 100644
--- a/src/language-js/print-preprocess.js
+++ b/src/language-js/print-preprocess.js
@@ -7,6 +7,7 @@ function preprocess(ast, options) {
     case "json-stringify":
     case "__js_expression":
     case "__vue_expression":
+    case "__vue_ts_expression":
       return {
         ...ast,
         type: options.parser.startsWith("__") ? "JsExpressionRoot" : "JsonRoot",
diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js
index e4fcc925d6a4..c01338f8b1c7 100644
--- a/src/language-js/printer-estree.js
+++ b/src/language-js/printer-estree.js
@@ -218,7 +218,10 @@ function printPathNoParens(path, options, print, args) {
         return [printDirective(node.expression, options), semi];
       }
 
-      if (options.parser === "__vue_event_binding") {
+      if (
+        options.parser === "__vue_event_binding" ||
+        options.parser === "__vue_ts_event_binding"
+      ) {
         const parent = path.getParentNode();
         if (
           parent.type === "Program" &&
diff --git a/src/main/comments.js b/src/main/comments.js
index 77d96158660a..baee3614c43f 100644
--- a/src/main/comments.js
+++ b/src/main/comments.js
@@ -200,7 +200,8 @@ function attach(comments, ast, text, options) {
       options.parser === "json" ||
       options.parser === "json5" ||
       options.parser === "__js_expression" ||
-      options.parser === "__vue_expression"
+      options.parser === "__vue_expression" ||
+      options.parser === "__vue_ts_expression"
     ) {
       if (locStart(comment) - locStart(ast) <= 0) {
         addLeadingComment(ast, comment);


Test patch:
diff --git a/tests/format/vue/ts-event-binding/__snapshots__/jsfmt.spec.js.snap b/tests/format/vue/ts-event-binding/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..d5c6398483e4
--- /dev/null
+++ b/tests/format/vue/ts-event-binding/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.vue format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<script setup lang="ts">
+let x = 1;
+function log(...args) {
+  console.log(...args);
+}
+</script>
+
+<template>
+  <div @click="if (x === 1 as number) { log('hello') } else { log('nonhello') };">{{ x }}</div>
+</template>
+
+=====================================output=====================================
+<script setup lang="ts">
+let x = 1;
+function log(...args) {
+  console.log(...args);
+}
+</script>
+
+<template>
+  <div
+    @click="
+      if (x === (1 as number)) {
+        log('hello');
+      } else {
+        log('nonhello');
+      }
+    "
+  >
+    {{ x }}
+  </div>
+</template>
+
+================================================================================
+`;
diff --git a/tests/format/vue/ts-event-binding/basic.vue b/tests/format/vue/ts-event-binding/basic.vue
new file mode 100644
index 000000000000..4eff06ddad7b
--- /dev/null
+++ b/tests/format/vue/ts-event-binding/basic.vue
@@ -0,0 +1,10 @@
+<script setup lang="ts">
+let x = 1;
+function log(...args) {
+  console.log(...args);
+}
+</script>
+
+<template>
+  <div @click="if (x === 1 as number) { log('hello') } else { log('nonhello') };">{{ x }}</div>
+</template>
diff --git a/tests/format/vue/ts-event-binding/jsfmt.spec.js b/tests/format/vue/ts-event-binding/jsfmt.spec.js
new file mode 100644
index 000000000000..2fd7eede986d
--- /dev/null
+++ b/tests/format/vue/ts-event-binding/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname, ["vue"]);
diff --git a/tests/format/vue/ts-expression/__snapshots__/jsfmt.spec.js.snap b/tests/format/vue/ts-expression/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..004db1fc1fb6
--- /dev/null
+++ b/tests/format/vue/ts-expression/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,45 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`basic.vue format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <p v-if="isFolder(file)">{{ (   file as   mymodule.Folder    ).deadline }}</p>
+</template>
+
+<script lang="ts"></script>
+
+=====================================output=====================================
+<template>
+  <p v-if="isFolder(file)">{{ (file as mymodule.Folder).deadline }}</p>
+</template>
+
+<script lang="ts"></script>
+
+================================================================================
+`;
+
+exports[`not-working-with-non-ts-script.vue format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <p v-if="isFolder(file)">{{ (   file as   mymodule.Folder    ).deadline }}</p>
+</template>
+
+<script></script>
+
+=====================================output=====================================
+<template>
+  <p v-if="isFolder(file)">{{ (   file as   mymodule.Folder    ).deadline }}</p>
+</template>
+
+<script></script>
+
+================================================================================
+`;
diff --git a/tests/format/vue/ts-expression/basic.vue b/tests/format/vue/ts-expression/basic.vue
new file mode 100644
index 000000000000..397f810cc541
--- /dev/null
+++ b/tests/format/vue/ts-expression/basic.vue
@@ -0,0 +1,5 @@
+<template>
+  <p v-if="isFolder(file)">{{ (   file as   mymodule.Folder    ).deadline }}</p>
+</template>
+
+<script lang="ts"></script>
diff --git a/tests/format/vue/ts-expression/jsfmt.spec.js b/tests/format/vue/ts-expression/jsfmt.spec.js
new file mode 100644
index 000000000000..2fd7eede986d
--- /dev/null
+++ b/tests/format/vue/ts-expression/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname, ["vue"]);
diff --git a/tests/format/vue/ts-expression/not-working-with-non-ts-script.vue b/tests/format/vue/ts-expression/not-working-with-non-ts-script.vue
new file mode 100644
index 000000000000..06afe1962e1c
--- /dev/null
+++ b/tests/format/vue/ts-expression/not-working-with-non-ts-script.vue
@@ -0,0 +1,5 @@
+<template>
+  <p v-if="isFolder(file)">{{ (   file as   mymodule.Folder    ).deadline }}</p>
+</template>
+
+<script></script>