Vue.js: Deploying to Production The joy that is Source Maps (with Vue.js and TypeScript)

Vue.js: Drawbacks of being a single

Published on Sunday, February 4, 2018 1:00:00 PM UTC in Programming

In my first post on Vue.js, I assumed you want to work with so-called single-file components (SFC). But after playing around with the for a while, I realized that this has major drawbacks too. Here are the details, and options to work around them.

Problems, problems, problems

The first thing I realized after a while is that the TypeScript compiler seemed to show random errors, until I realized it simply didn't pick up changes I introduced to my imports. So:

  • In a SFC, when you import some type A...
  • ... if you change the public API of A...
  • ... the SFC does not pick up the changes.

This is most annoying, because it means refactoring, renaming stuff and adding new features won't be picked up and show as errors. At first, I thought this is some sort of misconfiguration on my setup, but it is indeed a known issue of the Vetur extension, and has been for quite some time. Known workarounds are closing and re-opening Visual Studio Code (yeah, right...), or changing the language attribute of the script tag to something different, then change it back. This problem alone is so annoying that it has turned SFCs (at least in Visual Studio) useless for me, and I'm pretty sure I'm not alone with that.

There are some more issues though: the TypeScript version of Vetur seems to be hard-wired. In Visual Studio Code you can decide whether to use the built-in version or e.g. the one of your workspace, allowing you to instantly use newer versions and features even if Code has not built-in support for it yet. Sometimes, newer TypeScript version bring new features, and if Vetur does not recognize them, you're potentially flooded with wrong error messages. Example: in TypeScript 2.7, a feature named definite assignment assertions has been added - a most useful improvement, because it prevents common errors with frameworks that do data binding and watching of properties and objects (like Aurelia, Vue.js etc.). If you upgrade to TypeScript 2.7 however, your SFCs will look like this, for example when you start using the "definite assignment assertion modifer", or how others like to call it: the "damnit! operator".

image.png

This isn't limited to declarations; because TypeScript now starts confusing the actual types, the rest of your code will start looking like this:

image-1.png

Working like that, again, is no fun.

And, finally, another really annoying thing to me is that Visual Studio Code's really advanced features to auto-insert import statements are not working anymore. In most cases, Code adds imports as you type, and if that's not doing the trick you can always use Ctrl+. to get the suggestion list. The TypeScript integration of Vetur is not that clever; you have to add those imports all manually, which doesn't sound like a big deal, but it's really annoying once you got used to the comfort of the TypeScript language service.

Solutions?

Instead of putting your TypeScript sources inline in SFCs, you can always reference an external script. Like so:

// in ConfirmationDialog.vue:
<script lang="ts" src="./ConfirmationDialog.ts" />

Now that your code is in a separate, ordinary TypeScript file, all handling will be done by the TypeScript language service directly, and you'll have none of the above problems - yay!

Yay? Technically, you're still dealing with a single Vue component, you just separated its parts into individual physical containers. The problem you're now facing is that sometimes you need to reference the template part, (to include custom components in other components) and sometimes you need to reference the TypeScript implementation (the exported class). And that becomes a bit annoying too. Let's say you wanted to use the hypothetical "ConfirmationDialog" component in another component.

<confirmation-dialog-component ref="deleteConfirmationDialog">...</confirmation-dialog-component>

For that, you need to import the template and configure it:

import ConfirmationDialogComponent from "./components/ConfirmationDialog.vue";

@Component({
  components: { ConfirmationDialogComponent }
})
export default class SomethingSomething extends Vue { 

But if you also want to use features of the implementation, you would need a second import for the exported component class, like that (note that I'm not importing from the .vue file here!):

import ConfirmationDialog from "./components/ConfirmationDialog";

// ... later on
let result = await (<ConfirmationDialog>this.$refs.deleteConfirmationDialog).showDialog();

You gained the comfort of the best tooling available, but now you need to deal with two separate files for what is a single logical component, and because both need to be imported they also need to have different names (like ConfirmationDialog and ConfirmationDialogComponent above) - nasty.

A better solution!

With a bit of re-export kung fu we are able to "virtually" merge the two files into a single container again. To this end, do:

1. Change the export in your TypeScript implementation from a default export to a named one.

// in ConfirmationDialog.ts
@Component
export /* no default here */ class ConfirmationDialog extends Vue {
    // ...
}

2. Import the type in your Vue component file, and re-export it as default.

// in ConfirmationDialog.vue
<script lang="ts">
import { ConfirmationDialog } from "./ConfirmationDialog.ts";
export default ConfirmationDialog;
</script>

3. Work with the component like before, handling with a "single" file. I.e. in any consumer, you can once again do:

<template>
  <confirmation-dialog ref="deleteConfirmationDialog">...</confirmation-dialog>
</template>
import ConfirmationDialog from "./ConfirmationDialog.vue";

@Component({
    components: { UploadCard, ThumbnailCard, ConfirmationDialog }
})
export default class SomethingSomething extends Vue {
  // ... in some method:
  let confirmed = await (this.$refs.deleteConfirmationDialog as ConfirmationDialog).showDialog();
}

Nice! With just some changes to the way the TypeScript implementation is handled in the Vue component file, we have the best of both worlds: a consistent, single component to handle by consumers, and great tooling with the full feature set of the TypeScript language service.

Tags: TypeScript · Vue.js