Skip to content
Recipes to demystify Vue + TypeScript
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.gitignore Initial commit Jul 27, 2018
README.md Forgot to update TOC again Mar 6, 2019
package.json Initial commit Jul 27, 2018
yarn.lock Initial commit Jul 27, 2018

README.md

Vue + TypeScript Cookbook

If you're like me, you're busy and don't have a lot of time to fight with tools when they give you trouble—you just want to code and get stuff done! I ran into a lot of roadblocks learning Vue + TS, and after overcoming the last of the hurdles just recently, I decided to create this cookbook to help others who might have questions about how to hit the ground running with Vue + TS.

NOTE: This cookbook assumes you have a basic knowledge of TypeScript.

Table of Contents

Initial set-up

Setup is a breeze with the new vue-cli 3.x. Just create a new project:

$ vue create myapp

When prompted, choose to manually select features, and make sure TypeScript is chosen from the list.

Vue CLI v3.0.0-rc.7
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features

For the purposes of this cookbook, we'll opt to not use class-style components and stick with the standard functional-based format.

What are the basic need-to-knows?

The main thing you'll need to know is that your script blocks will change. Without TypeScript, they look something like this:

<script>
export default Vue {
  data() {
    return {
      name: '',
      locations: [],
    };
  },

  methods: {
    test(name) {
      return name + '!';
    },
  },
}
</script>

With TypeScript, they'll look like this:

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  data() {
    return {
      name: '',
      locations: [] as string[],
    };
  },

  methods: {
    test(name: string): number {
      this.locations.push(name);
      return this.locations.length;
    },
  },
});
</script>

I'm using Vuex mapState or mapGetters, and TypeScript is saying the mapped state/getters don't exist on this.

This is a known bug in Vuex, and there's an outstanding PR. The bug only presents itself when using an object spread and including one or more computed properties:

computed: {
  ...mapState(/*...*/),
  someOtherProp() {}
}

To avoid this, either avoid the spread:

computed: mapState(/*...*/)

or define an interface for the Vuex bindings and apply it to your component:

import Vue, { VueConstructor } from 'vue';
import { mapState } from 'vuex';
import { MyState } from '@/store';

interface VuexBindings {
  stateVar: string;
}

export default (Vue as VueConstructor<Vue & VuexBindings>).extend({
  data() {
    return {
      name: '',
      locations: [] as string[],
    };
  },

  computed: {
    ...mapState({
      stateVar: (state: MyState) => state.stateVar,
    }),
    nothing(): string {
      return 'test';
    },
  },

  methods: {
    test(name: string): number {
      console.log(this.stateVar); // no more TS error
      this.locations.push(name);
      return this.locations.length;
    },
  },
});

How do I make a function outside the scope of the Vue component have the correct this context?

You might be thinking, "why not just make a method within the Vue component?" The thing is, TypeScript is picky about referring to computed properties, methods, and data variables from within the data method, even if you're referring to this within a callback handler which is perfectly valid. So you'll have to put functions outside the scope of the Vue component. Here's what that looks like:

import Vue from 'vue';

type ValidatorFunc = (this: InstanceType<typeof HelloWorld>) => Function;

const validator: ValidatorFunc = function() {
  return () => {
    if (this.name === 'bad') {
      // do something
    }
  };
};

const HelloWorld = Vue.extend({
  data() {
    return {
      name: '',
      locations: [] as string[],
      somethingHandler: {
        trigger: 'blur',
        handler: validator.bind(this),
      },
    };
  },
});

export default HelloWorld;

How do I annotate my $refs to avoid type warnings/errors?

Some UI libraries will let you slap refs on their components to make direct method calls. Here's how you give type awareness to those refs.

First, an example of a quick one-off approach:

export default Vue.extend({
  methods: {
    test() {
      (this.$refs.dataTable as ElTable).clearSelection();
    },
  },
});

If you refer to the same ref many times, or you have several refs, it's easier to create an interface:

import Vue, { VueConstructor } from 'vue';
import { ElTable } from 'element-ui/types/table';
import GoogleMap from '@/components/shared/GoogleMap.vue';

// With this, you won't have to use "as" everywhere to cast the refs
interface Refs {
  $refs: {
    name: HTMLInputElement
    dataTable: ElTable,
    map: InstanceType<typeof GoogleMap>;
  }
}

export default (Vue as VueConstructor<Vue & Refs>).extend({
  ...
})

How do I properly annotate mixins?

When writing mixins, you'll want to extend Vue like you would with a component:

import Vue from 'vue';

export const myMixin = Vue.extend({
  data() {
    return {
      counter: 0,
    };
  },

  methods: {
    increase(by: number = 1) {
      this.counter += by;
    },
  },
});

Then in any Vue component you wish to use your mixin, make sure you extend the component's type definition with your mixin:

export default (Vue as VueConstructor<
  Vue & InstanceType<typeof myMixin>
>).extend({
  mixins: [myMixin],

Now all of your mixin's methods/data/properties will be recognized, fully typed.

Conclusion

If something's been bugging you with Vue + TypeScript, please open an issue to discuss having a recipe added!

You can’t perform that action at this time.