Search code examples
typescriptvue.jsjsxrenderingvue-cli

Getting Vue.js to render JSX with TypeScript from base CLI install with JSX package


The Setup

I've created a basic project with the vue-cli, during which I used the "Manually select features" to install, where I picked the following (besides a linter):

  • Selected Babel, TypeScript, Vuex, and Linter / Formatter
  • Selected 2.x
  • No to class-style components
  • Yes to Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?

Then afterwards, I also installed the jsx support (and I verified the Babel version installed was 7, since that is required for that jsx support repo).

In my bable.config.js file I added (did not replace what was generated by the vue-cli) the preset requested by the jsx repo:

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    '@vue/babel-preset-jsx', //<-- Added this
  ],
};

I've confirmed that the auto-generated tsconfig.json has the needed configuration (at least as I understand it):

"compilerOptions": {
    ...
    "jsx": "preserve",
    ...
}

I've confirmed that the auto-generated shims-tsx.d.ts file has the needed configuration (at least as I understand it):

import Vue, { VNode } from 'vue';

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode {}
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any
    }
  }
}

In the resulting auto-generated main.ts file, the JSX appears to be successfully used (even though it is not a .tsx extension, as I believe webpack, behind the scenes, is transforming things to work:

new Vue({
  store,
  render: (h) => h(App),
}).$mount('#app');

So the auto-generated App.vue file has this (I'm ignoring the <style> info), which renders fine on npm run serve:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

<script lang="ts">
import Vue, {VNode} from 'vue';
import HelloWorld from './components/HelloWorld.vue';

export default Vue.extend({
  name: 'App',
  components: {
    HelloWorld,
  },
});
</script>

The Issues

It is my understanding that the following modification to App.vue should then work (the render function is exactly what the first "Syntax" example in the jsx repo has):

// template removed

<script lang="ts">
import Vue, {VNode} from 'vue';
import HelloWorld from './components/HelloWorld.vue';

export default Vue.extend({
  name: 'App',
  functional: true,
  render() {
    return <p>hello</p>
  }
});
</script>

But instead, I get this set of errors in the terminal window:

ERROR in /home/scott/convrrt-component/view-play/src/App.vue(19,3):
19:3 No overload matches this call.
  The last overload gave the following error.
    Type '() => boolean' is not assignable to type '(createElement: CreateElement, hack: RenderContext<Record<string, any>>) => VNode'.
      Type 'boolean' is not assignable to type 'VNode'.
    17 |     HelloWorld,
    18 |   },*/
  > 19 |   render() {
       |   ^
    20 |     return <p>hello</p>
    21 |   }
    22 | });
ERROR in /home/scott/convrrt-component/view-play/src/App.vue(20,13):
20:13 Cannot find name 'p'.
    18 |   },*/
    19 |   render() {
  > 20 |     return <p>hello</p>
       |             ^
    21 |   }
    22 | });
    23 | </script>
ERROR in /home/scott/convrrt-component/view-play/src/App.vue(20,15):
20:15 Cannot find name 'hello'.
    18 |   },*/
    19 |   render() {
  > 20 |     return <p>hello</p>
       |               ^
    21 |   }
    22 | });
    23 | </script>
Version: typescript 4.1.6

I thought maybe I needed to explicitly use the h() function, so I tried this for the render function (sort of matching to what the main.ts was doing):

  render(h, ctx) {
    return h(<p>hello</p>)
  }

That did not work. The last two errors are the same as above, but the first error changes to this:

ERROR in /home/scott/convrrt-component/view-play/src/App.vue(20,14):
20:14 No overload matches this call.
  Overload 1 of 2, '(tag?: string | VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<...>> | AsyncComponentPromise<...> | AsyncComponentFactory<...> | (() => Component<...>) | undefined, children?: VNodeChildren): VNode', gave the following error.
    Argument of type 'boolean' is not assignable to parameter of type 'string | VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<...>> | AsyncComponentPromise<...> | AsyncComponentFactory<...> | (() => Component<...>) | undefined'.
  Overload 2 of 2, '(tag?: string | VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<...>> | AsyncComponentPromise<...> | AsyncComponentFactory<...> | (() => Component<...>) | undefined, data?: VNodeData | undefined, children?: VNodeChildren): VNode', gave the following error.
    Argument of type 'boolean' is not assignable to parameter of type 'string | VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<...>> | AsyncComponentPromise<...> | AsyncComponentFactory<...> | (() => Component<...>) | undefined'.
    18 |   },*/
    19 |   render(h, ctx) {
  > 20 |     return h(<p>hello</p>)
       |              ^
    21 |   }
    22 | });
    23 | </script>

I've tried a few other things that I cannot quite recall now (explicit typing on the render was one, I think, both on the function itself and on the return value).

Question: What setup/configuration am I still missing or what syntax issue do I have that is causing this to still fail to render the return as JSX and pass the TypeScript gauntlet?


Solution

  • JSX enabled by default

    You don't need to install anything for JSX support in a Vue CLI scaffolded project. @vue/cli-plugin-babel/preset includes @vue/babel-preset-app, which already includes @vue/babel-preset-jsx.

    Appending '@vue/babel-preset-jsx' to the Babel presets introduces errors (likely due to duplication of preset settings already in place by default). You should remove that:

    module.exports = {
      presets: [
        '@vue/cli-plugin-babel/preset',
        // '@vue/babel-preset-jsx', // XXX: Remove this
      ],
    };
    

    VS Code Extensions caveat

    If using Vetur or VueDX in VS Code, you'll notice TypeScript errors reported on the JSX lines. Use <script lang="tsx"> to avoid these errors:

    <script lang="tsx">
    import Vue from 'vue'
    
    export default Vue.extend({
      render() {
        return <b>hello</b>
      }
    })
    </script>