How to Develop Vue Components in TypeScript
2019. 3. 28.

The Journey Here
The first time I worked on an SPA project was in 2012. It was a fairly large-scale project built based on Backbone, which was the hottest thing back then. At that time, the front-end development environment wasn't as good as it is now, and the concept of SPA itself was just starting to gain traction. The front-end landscape was dominated by frameworks that couldn't break away from MVC. While there were heated debates about whether AngularJS was MVC or not, a subspecies called React emerged. This framework, influenced by reactive and functional paradigms, grew rapidly. It stings a bit that the developers who led this are younger than me. Then, after a huge leap, Vue appeared. Vue was influenced by various frameworks. Its code looked similar to React, but it actively utilized reactivity in terms of state management. Vue grew into a framework rivaling React in popularity, mainly because it was easy to use. Even if web developers understood the underlying concept of immutability in React and Redux, they likely faced many hurdles when applying it in practice. Vue's usage rate continues to increase day by day.
The front-end community's thirst led to overcoming the linguistic shortcomings of JavaScript. CoffeeScript, Flow, Babel, and Microsoft's TypeScript are such efforts. Of course, Babel is a tool that enhances compatibility with standard specifications, so it can be seen as having a different character, not independently improving the language itself. Anyway, CoffeeScript, which initially led this field, is making a grand exit after leaving the great achievement of passing on several outstanding concepts to ES6. Flow and TypeScript, as supersets of JavaScript (ECMAScript), tried to apply strict typing on top of the existing syntax. They introduced a verification step corresponding to compile-time to an interpreter language that only has a runtime. In the unseen fierce battle between Flow and TypeScript, TypeScript ultimately won, becoming the undisputed leader among transpiled languages.
Personally, I don't hold a positive view on strict typing. However, I do believe that explicit module signatures at the level of protocols or interfaces for polymorphism are definitely helpful. Although opinions are divided, the once strange language called JavaScript is gradually evolving into a decent language. If one were to criticize this language for lacking types, it would truly be a language with no redeeming qualities. It can do anything, yet it's neither here nor there, and I think that very point is its advantage. I don't know if TypeScript will become the mainstream or another CoffeeScript. However, as a front-end developer, I thought interest in and basic learning of TypeScript were necessary. So, I invested time in the early days to learn it to some extent. Even after roughly grasping the language's understanding and concepts, I wasn't very fond of TypeScript and had no particular intention of using it in practice.
Around the time TypeScript was fading from my memory, years after first learning it, interest in it was growing. Even within the company, there were many cases where it was applied in practice by developers who were not front-end developers. This indicates that TypeScript's domain had expanded. Developers accustomed to strictly-typed languages chose TypeScript as an intermediate buffer when they had to deal with JavaScript. Using types isn't just a matter of using syntax or not; it affects the entire application design, so I could fully sympathize. Although there were requests from within the company, the team also needed expertise in TypeScript. Therefore, we decided to use TypeScript for the official version of the project that was currently under development as a prototype. That project used the Vue framework for the same reason as TypeScript.
The Vue camp is quite favorable towards TypeScript, to the extent that the codebase for the currently developed version 3.0 is being entirely converted to TypeScript. Although still in the 2.x version range, I expected Vue and TypeScript to pair well. The first dilemma in the project was choosing between object-based components and class-based components. Judging that I couldn't choose based on initial experience, we decided to proceed with object-based components for those corresponding to the bottom-level Base Components, and class-based components for the rest. The number of base components was smaller. This was based on the speculation that we would eventually move towards class-based components. As I found out during the process, Vuex also played a role in this decision. This post is written to share the experience of this choice and the overall results.
JavaScript and Vue Components
The introduction was long. My excuse for such a long intro is the feeling of how much times have changed. The code I write now is contained in files with a structure called SFC, which browsers cannot understand, and the language isn't even JavaScript. The code visible when debugging in the browser isn't the code the browser actually uses, but rather ghost-like code reconstructed by source maps. This situation isn't particularly special as it's been like this for a while, but it still makes me feel the passage of time. Actually, feeling the passage of time in front-end development is nothing new.
SFC (Single File Component) is the dedicated file format for Vue components recommended by Vue. It defines the template, JavaScript, and CSS all within a single file. When developing components, Vue encourages developers to define options that can create a class, rather than defining the class directly.
<template>
<div>
<input type="text" v-model="newTodo" @keyup.enter="onEnter">
<ul>
<li v-for="todo in todos" :key="todo">{{todo}}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
todos: ['TASK1'],
newTodo: ''
};
},
methods: {
onEnter(ev) {
this.addTodo(this.newTodo);
},
addTodo(title) {
this.todos.push(title);
}
}
};
</script>
I implemented a simple Todolist component. The data holding the list of Todos is an array named todos
. Tasks are stored as strings within the array. The template iterates through todos
and renders the Todolist using li
elements. When you type a new task in the input box and press Enter, the todos
array is updated. It's a component with simple functionality, and it works correctly. Now, let's try applying TypeScript here.
Vue.extend
There are two ways to apply TypeScript to Vue components: using Vue.extend
to create an object, and creating a Class. In the project I'm currently working on, I've used both, distinguishing them based on the component's characteristics to compare their pros and cons. Let's first discuss the method using Vue.extend
. Defining a component using Vue.extend
is almost identical to defining a component without TypeScript.
<template>
<div>
<input type="text" v-model="newTodo" @keyup.enter="onEnter">
<ul>
<li v-for="todo in todos" :key="todo">{{todo}}</li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
todos: ['TASK1'],
newTodo: ''
};
},
methods: {
onEnter(ev: UIEvent) {
this.addTodo(this.newTodo);
},
addTodo(title: string) {
this.todos.push(title);
}
}
});
</script>
Just like defining a Vue component in JavaScript, you define the component using a component creation options object. However, by using the type-declared Vue.extend
, you can benefit from type assistance. In VS Code, you can simply hover over extend
to see type information, and use Peek Definition to see all possible signatures extend
can have, as shown in the image below.
(Image description: A VS Code Peek Definition window showing the various overloaded type signatures for Vue.extend)
Thanks to the type declarations defined in the Vue project's @type
directory, using extend
provides TypeScript assistance when developing components[1]. Warnings for disallowed component options and incorrect usage of APIs or component members work correctly. Data types are also inferred correctly[2]. During the initial testing phase, this proceeded very smoothly. However, I soon discovered a major problem.
interface Todo {
title: string;
}
I declared a Todo
type to be used for the component's data. I expected to be able to use the Todo
type for the type
property when defining a prop in the component, intended to receive Todo
type data from a parent component. Without any suspicion, I used Todo[]
to signify using the generic type Todo
within an array.
export default Vue.extend({
props: {
todos: {
type: Todo[], // Expecting this to work
required: true,
default: []
}
},
...
However, this code soon resulted in an error.
(Image description: A code snippet showing a TypeScript error message: "'Todo' only refers to a type, but is being used as a value here.")
The error message states that Todo
, which should be used as a type, is being used as a value. That is, type: Todo[]
didn't declare a type, but assigned the value Todo[]
to type
. Thinking about it, syntactically, this made sense. Also, TypeScript types only provide assistance during development or compilation and don't remain in the actual transpiled JavaScript code, so they cannot be used as values. When using basic JavaScript types like Number
or Array
, it was possible because these also exist as actual values. Searching online reveals discussions about several methods, but none seem satisfactory yet.
This critical drawback—the inability to use TypeScript types for the type
of props
when defining components as objects in TypeScript—currently has no solution I've found. In the ongoing project, only the terminal base components are defined as objects, and due to this issue, we don't use types but instead receive them using basic JavaScript types.
export default Vue.extend({
props: {
title: {
type: String, // Using basic JS type constructor
required: true,
default: []
}
},
Integrating Vuex brought another problem. This issue manifests differently in class-based components as well. Vuex's mapping helpers easily map various data-related functions defined in the store to component members. When working with JavaScript, I used mapping helpers for almost all store functionalities. However, when using mapping helpers in TypeScript, methods and data get laundered into String values and other mapping helper options, preventing TypeScript from inferring that the member exists in the component.
methods: {
...mapActions(['addTodo']),
onEnter(ev: UIEvent) {
this.addTodo(this.newTodo); // Error: Property 'addTodo' does not exist
}
}
(Image description: A code snippet showing a TypeScript error on
this.addTodo
, indicating the property does not exist on the component type)
Therefore, mapping helpers cannot be used effectively. You have to access the store directly by creating indirection methods or, in the case of actions, executing dispatch
directly.
methods: {
addTodo(todo: string) {
this.$store.dispatch(‘addTodo’, todo);
},
onEnter(ev: UIEvent) {
this.addTodo(this.newTodo); // This now works
}
}
I concluded that "Vue.extend
is not a good combination with TypeScript".
Class based component
While the method using Vue.extend
and objects had issues with TypeScript, I also thought that in a TypeScript environment, it might be more appropriate to write components as classes[3]. I felt Vue.extend
leaned more towards the Vue framework, whereas Class-based components leaned more towards the language itself. Although ES6 also allows Class-based components, Vue was designed based on defining components using component creation option objects, so currently, the object form is most suitable even in ES6. I don't know how it will be from version 3 onwards, but I believe this aligns with Vue's intended direction. Class-based Vue components are still in a phase where the Vue community is finding solutions rather than having established ones[8]. Perhaps if it weren't for TypeScript, class-based components wouldn't have been considered at all.
<template>…</template>
<script lang="ts">
import {Component, Vue, Prop} from 'vue-property-decorator';
import {mapActions} from 'vuex';
@Component({
methods: {
...mapActions(['addTodo']) // Still problematic
}
})
export default class Todolist extends Vue {
public newTodo: string = '';
// Type assertion needed for mapped action
public addTodo!: (title: string) => void;
@Prop({required: true})
public todos!: Todo[]; // Type works perfectly here!
public onEnter(ev: UIEvent) {
this.addTodo(this.newTodo);
}
}
</script>
In classes, computed
properties are defined using get
and set
, methods are used directly as class methods, and data fields within the class are used directly as Vue data. Other Vue concepts like watcher
and props
use decorators[8]. Decorators provide Vue options to class-based components. By moving to class-based components, the issue with component property types was resolved very cleanly[4].
@Prop({required: true})
public todos!: Todo[];
Using the Prop
decorator defines todos
as a prop and passes prop options as arguments. The type can then be declared using TypeScript's proper syntax, not as an option[8]. Although the vue-property-decorator
containing the Prop
decorator is not an official Vue module, I find it a stable and clear method. Now, when using the todos
prop in the component, we can get type assistance.
However, this time too, problems arose when using Vuex mapping helpers. To use the mapAction
helper in the component, the Component
decorator was used.
@Component({
methods: {
...mapActions(['addTodo'])
}
})
Let's assume the addTodo
action in the store has its signature correctly defined using TypeScript types. But once again, because addTodo
gets laundered through the mapping helper's string argument, the component cannot infer its existence. Trying to use addTodo
directly in the component results in a type error stating that a non-existent method is being used. Unlike Vue.extend
, where there was no way to use mapping helpers, class-based components offer a workaround: define the signature again within the class.
public addTodo!: (title: string) => void;
This essentially forces the fit by defining the method's signature redundantly. The problem with redundancy exists not only in code with logic but also in type definitions. Type definitions should be made once, allowing the compiler to infer them. I thought about and searched for various alternatives, but couldn't find any significantly different methods.
Conclusion
After much deliberation, I concluded that code duplication must be avoided, and decided not to use mapping helpers. The number of store elements mapped by helpers can be substantial depending on the component, and defining types redundantly unnecessarily complicated the code. Considering the response needed for type changes, this approach seemed untenable. The components in the current project look like this:
<template>…</template>
<script lang="ts">
import {Component, Vue, Prop} from 'vue-property-decorator';
@Component
export default class Todolist extends Vue {
public newTodo: string = '';
@Prop({required: true})
public todos!: Todo[] // Type defined using decorator and TS
// Accessing state directly or via computed property
get schedule(): Schedule {
return this.$store.state.schedule;
}
public onEnter(ev: UIEvent) {
// Dispatching action directly
this.$store.dispatch('addTodo', this.newTodo);
}
}
</script>
Besides not using mapping helpers, we established the following conventions for using Vuex:
- Actions or mutations, unless in special cases, are not used via indirection methods but called directly using
dispatch
andcommit
methods respectively. Although type assistance might not be available if incorrect types are used as arguments, this eliminates type duplication. At least if an action or mutation with an incorrect name is executed, the framework will report an error, which is sufficient. - State or getters are defined as computed properties depending on the situation, or accessed directly via
$store
, but using$store
in templates is avoided to prevent increasing template complexity.
Adhering to these conventions, we are currently developing Vue components in a TypeScript environment without further hesitation. Since no significant issues have arisen towards the end of the project, it seems likely we will maintain this approach until Vue 3 is released.
At this point, I think TypeScript fits better with React than with Vue. Besides what's been mentioned, there's a significant difference supporting this view. TypeScript supports JSX under the name TSX, enabling type checking even for JSX components, whereas Vue's SFCs or templates are not yet supported. Therefore, no matter how well component Props types are specified, it's still only partial usage[1]. TypeScript and the Vue framework seem to need more time. However, the community favors TypeScript to the extent that the entire codebase for Vue 3 is being developed in TypeScript. Perhaps Vue 3 will offer better methods[1]. Recently, a Class API RFC was added to the RFC documents discussing Vue's improvement directions. While further discussion is needed, the overall content looks quite promising. If interested, please refer to it. If you have any good alternatives to suggest, they are always welcome.(shirenbeat@gmail.com)
with kakaopay
Recommend Post
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.