Blog post cover
Arek Nawo
01 Mar 2021
10 min read

Vue with TSX - the perfect duo!?

From all the available JavaScript frameworks, I’d say that Vue and React are my favorites. React for its vast ecosystem, great TypeScript support, JSX, and Vue for its simplicity, performance, and some smaller life quality improvements, like auto-passing props, easy-to-use transitions system, directives, slots & more.

So, how about getting the best of both worlds? Get Vue’s benefits in React would mean making React into Vue, so that’s not an option. How to use TypeScript and JSX in Vue then?

Well, Vue is heavily reliant on its custom templating syntax and SFCs (Single File Components). However, it’s also very “unopinionated” and provides its users with many ways of doing the same thing. This includes using JSX, even TSX (JSX with TypeScript)! Sadly, when you do many things well, you usually can’t do a single thing great. Let me demonstrate that through Vue 2’s TSX support.

Vue 2 TSX Support

Let’s start with pure JSX. A quick search on Vue 2 docs brings the following results:

The entirety of Vue 2 docs on JSX
The entirety of Vue 2 docs on JSX

There’s certainly not a lot of it, but potentially that’s all we need? The link redirects us to a GitHub README, detailing the installation of the required Babel plugin and the general usage of JSX with other Vue features.

Add required dependencies first (I’m using Yarn here):

yarn add --dev @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

Configure Babel:

module.exports = {
  presets: ["@vue/babel-preset-jsx"],
};

Now, if you’re lucky, you should be able to just jump into your code and write JSX. If you’re not, you’ll encounter a few errors on the way. Most of them should be easy to fix by browsing the Babel plugin’s GitHub issues or searching elsewhere on the web. However, resources on this subject are quite limited relative to Vue’s popularity.

When you eventually get to writing your Vue JSX code, things should be pretty good from here. Your JSX should be compatible with SFCs, and you shouldn’t even have to use the components field for them to work!

import HelloWorld from "./components/HelloWorld.vue";

export default {
  render() {
    return (
      <div id="app">
        <img alt="Vue logo" src="./assets/logo.png" width="25%" />
        <HelloWorld msg="Hello Vue in CodeSandbox!" />
      </div>
    );
  },
};

TypeScript

With JSX in place, we can proceed to replace the “J” with “T” by bringing TypeScript into play! Using the same method as before, we’ll first set up TypeScript alone.

Now, Vue 2 docs have a whole page dedicated solely to TypeScript support, so I won’t complain there. It details the configuration process, caveats, and different ways in which you can use TS with Vue (“unopinionation” - remember?)

There are 2 main ways to use TS with Vue - the basic one and the “class-based” one.

For the “class-based” approach, I must admit that I’m not really into it. Maybe it’s because I’m more into the “functional-style” programming lately, or maybe because I don’t like ES decorators. For me, they’re too experimental right now, and - in TypeScript - they don’t provide the same auto-completion as all the other “well-established” features. Anyway, there’s a whole site dedicated to the “class-based” Vue components and their use with TypeScript, so feel free to check it out if you’re interested.

As for the basic approach, you simply wrap your component object inside Vue.extend() , and that’s it. You’ll still need to use to annotate your props, render() method, and all computed properties with PropType<T>, VNode, and your type of choice, respectively.

import Vue, { PropType, VNode } from "vue";
import HelloWorld from "./components/HelloWorld.vue";

export default Vue.extend({
  props: {
    message: [String, Function] as PropType<string | (() => string)>,
  },
  render(): VNode {
    return (
      <div id="app">
        <img alt="Vue logo" src="./assets/logo.png" width="25%" />
        <HelloWorld
          msg={typeof this.message === "string" ? this.message : this.message()}
        />
      </div>
    );
  },
});

The snippet above is an adaptation of the previous one for TypeScript. I left the JSX, but there are a few issues with it that I’ll get into. But before that, I wanted to mention a “shim” file which you’ll need for TypeScript to not freak out on *.vue file imports:

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

Put it in in a *.d.ts file in your root folder, within a directory that’s includeed in your TS project, or specify directly in tsconfig.json types property - your choice.

However, if you’ve worked with TypeScript declaration files before, you might notice how the above “shim” is flawed. With it, TS will accept *.vue SFCs, but nothing except general Vue properties, will be type-safe. No props, computed, methods, etc. - nothing! There’s no way around it. You might still get some autocompletion in your code editor / IDE, but that’s only thanks to some patching done on the side - no “pure TypeScript.”

TSX combo

It’d seem like, by combining the JSX and TypeScript setups, we should have TSX ready to go. Sadly, it’s not that simple.

You’ll certainly need some more shims to adapt the JSX typings for Vue. Something like the following will do:

import Vue, { VNode } from "vue";

declare global {
  namespace JSX {
    interface Element extends VNode {}
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

You’ll also need to adjust your tsconfig.json by setting jsx to ”preserve”. This will ensure that no TS-related processing will touch the JSX syntax, allowing it to safely reach Babel for proper handling (provided that there aren’t any errors and issues with your setup).

Alright, now you should be good to go! Enjoy your top-notch Vue TSX experience… yeah. Let’s talk about that.

The reason I’ve just walked you through the generic Vue 2 TSX setup isn’t really to showcase “how to” do it. It’s to show you “how flawed” the whole process is. Sure, it might be useful when converting an existing project or boilerplate, but most of the time, you’ll do just fine with Vue CLI and a proper set of plugins or a no-config bundler like Parcel or Poi. However, this won’t fix the plethora of potential setup issues or lack of in-depth documentation. More than that, none of these ways will grant you proper TSX experience, as you might know it from React.

Editor support

Talking about the experience, what it’s like? After spending some time with it, I must admit - it’s not good. In terms of editors, there are only 2 players that count - VS Code and WebStorm. I used Vue with Vetur extension. However, it doesn’t matter, as this extension focuses mostly on SFCs support (it lacks in this area too). There’s also the VueDX extension that works much better than Vetur for SFCs, but it completely breaks the JSX/TSX support, so I had to disable it.

Now, both editors were close, with WebStorm coming slightly ahead. Props autocompletion was mostly non-existent, and while working within components, all. this-related interactions were hit or miss, however generally better on WebStorm.

Vue-tsx-support

Now, where there’s a problem, there’s also a solution. And for Vue 2 TSX support, it’s a “support library” named vue-tsx-support. It’s a (mostly) TS-only library of types, casting function, utilities, etc., meant to properly type your Vue components and make them work nicely with TSX. I won’t be going too much into detail here (the GitHub README is pretty in-depth), I would like to show you a usage example of this library:

import HelloWorld from "./components/HelloWorld.vue";
import * as tsx from "vue-tsx-support";

interface HelloWorldProps {
  msg?: string;
}

const TypedHelloWorld = tsx.ofType<HelloWorldProps>().convert(HelloWorld);

export { TypedHelloWorld as HelloWorld };

You can see how the ofType() and convert() casting functions are used to get type-safe Vue components with all the autocompletion goodness.

And with that, I’d say the vue-tsx-support library is the best way to use Vue 2 with TSX right now, and probably forever. Sure, it is a bit verbose, but it’s the only way to get the proper TSX experience in such a setup.

The coming of Vue 3

Now, forget all you’ve read until this point, as it doesn’t matter. Alright, maybe I’m joking, but with Vue 3 being in “stable beta” right now, the Vue-TSX combo is set to become much more prominent in the Vue space.

Honestly, that’s what’s the title is all about. I’ve been using Vue 3 extensively for a while now while developing my product - CodeWrite (blogging tool for developers). And yes, I did use it with TSX, and the development experience was just perfect!

Vue 3 is now written in TypeScript, giving its support a major boost, and with the new Composition API, it seems like the issue of this has finally been resolved. Sure, the TS and JSX docs haven’t changed much in terms of their clarity, but the setup process as a whole seems to be much less cumbersome.

Setup and configuration

In a nutshell, you once again have to add jsx: “preserve” to your tsconfig.json and do some changes in your Babel config (it’s a plugin this time):

module.exports = {
  plugins: ["@vue/babel-plugin-jsx"],
};

But this time, that’s it! You can now write your Vue components in TSX, with great autocompletion in both VS Code and WebStorm!

Take a look at an example component (TSX + Composition API):

import { defineComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";

export default defineComponent({
  props: {
    message: [String, Function] as PropType<string | (() => string)>,
  },
  setup(props) {
    return () => {
      const message = props.message;
      return (
        <div id="app">
          <img alt="Vue logo" src="./assets/logo.png" width="25%" />
          <HelloWorld msg={typeof message === "string" ? message : message()} />
        </div>
      );
    };
  },
});

For SFCs to work, you’ll still require shims (although different ones), and auto-completion from those might not be perfect, but with this setup in Vue 3 - I don’t know why you’d even use those.

declare module "*.vue" {
  import { ComponentOptions } from "vue";

  const component: ComponentOptions;

  export default component;
}

Again, the configuration shouldn’t be much of a problem with all the available tools out there. Vite is a noteworthy newcomer, straight from the Vue core team. It sports a super-fast ESM workflow and supports TypeScript, JSX, and TSX with little to no configuration.

Should you transition?

Now, whether you’re willing to jump right into Vue 3 right now depends solely on you. I did because the much-improved TypeScript and JSX support was worth it for me. Also, I consider the new Composition API much superior to the prior one (Option API) and prefer Vue’s reactivity model to React’s.

However, Vue 3 is backward-incompatible due to some breaking changes, and thus its ecosystem is much, much smaller. It wasn’t a big issue for me, but I know it can be for many. So again, your choice.

Stay tuned!

So, with my experience of Vue 3, you can be sure there’s more great new content coming. To stay up-to-date with it, follow me on Twitter, Facebook, or through my newsletter. Also, if you’re interested in starting your own technical blog (and checking out what you can do with Vue 3 as well), give CodeWrite a try - for free!

Thanks for reading, and happy coding!

If you need

Custom Web App

I can help you get your next project, from idea to reality.

© 2024 Arek Nawo Ideas