Repository: prettier/prettier (JavaScript)

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.


Hints:
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.

Patch:
diff --git a/src/language-html/embed.js b/src/language-html/embed.js
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- 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
--- /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
--- /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
--- /dev/null
+++ b/tests/format/vue/ts-event-binding/jsfmt.spec.js
@@ -0,0 +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
--- /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
--- /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
--- /dev/null
+++ b/tests/format/vue/ts-expression/jsfmt.spec.js
@@ -0,0 +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
--- /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>