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.
- Initial set-up
- What are the basic need-to-knows?
- I'm using Vuex
mapState
ormapGetters
, and TypeScript is saying the mapped state/getters don't exist onthis
. - How do I make a function outside the scope of the Vue component have the correct
this
context? - How do I annotate my
$refs
to avoid type warnings/errors? - How do I properly annotate mixins?
- When I use an Array prop type, my component's properties (data, computed properties, etc) are shown as type errors!
- How to enable type check on default values and validator functions of a property?
- Conclusion
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.
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 (more info in this 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;
},
},
});
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;
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({
...
})
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.
When I use an Array prop type, my component's properties (data, computed properties, etc) are shown as type errors!
This is a bug in Vue 2.x, and is easily resolved by using PropType
:
import Vue, { PropType } from 'vue';
import { Product } from '@/interfaces/product';
export default Vue.extend({
name: 'MyComponent',
props: {
products: {
type: Array as PropType<Product[]>,
required: true,
},
},
});
Like PropType
, there is also PropOptions
, which enables you to set the type information for the whole property, not only the type definition itself.
import Vue, { PropOptions } from 'vue';
import { Product } from '@/interfaces/product';
export default Vue.extend({
name: 'MyComponent',
props: {
products: {
type: Array,
default: () => [],
validator: function (value) {
// ... your validation code
}
} as PropOptions<Product[]>
},
});
If something's been bugging you with Vue + TypeScript, please open an issue to discuss having a recipe added!