Vue.js is a notoriously flexible framework with applications ranging from progressively enhancing static pages to building full on single page applications. Likewise, Vue also provides the flexibility of developing in either plain ol’ JavaScript or in it’s more strict type-safe counterpart, TypeScript.
That begs the question, should you be using TypeScript in your Vue.js powered application? In this article, let’s examine some of the reasons you might choose to do so.
Since TypeScript is a strongly typed language, it is able to detect a much wider range of issues right in your IDE. The alternative to this is finding out about said issues only after the code is run in the browser. This could mean catching the issue during manual testing or only after an end user reports it.
For example, let’s say you wanted to format a 9 digit phone number to have dashes for display purposes.
<script setup>
import { ref, computed } from "vue";
const phone = ref("55566677777");
const [match, one, two, three] = phone.value.match(/^(\d{3})(\d{3})(\d{4})$/);
// 555-666-7777
const formatted = computed(() => ${one}-${two}-${three}
);
</script>
<template>Phone: {{ formatted }}</template>
I’ve intentionally left off lang="ts"
thus far to show what would happen with plain JavaScript. Now, the above would work just as expected, but maybe you were in a hurry, or the phone number is actually in a different file, or it’s the result of a function. Long story short, what if the phone number ends up being an actual number instead of a string?
const phone = ref(55566677777);
Working in my IDE, there is no indication of an issue.
This screenshot is directly from VS Code and I can’t tell anything is wrong. There are no red squiggly lines in sight and I’m even using ESLint. We do have a yellow squiggly line under match but that’s only because we aren’t using the variable, it’s not cause for error.
If I hop on over to the browser though, immediately things break down. The whole app stops working and I get this error in the console.
The issue is that numbers don’t have a match method like strings do. Ok, that’s an easy enough fix. Back in the IDE we can wrap the number in quotes again but in doing so we accidentally add another number to it.
const phone = ref("555666777777");
After making the change we go back to the browser and run things again by refreshing the page. This time we get another error.
This time match is null because the string didn’t match the regex and we can’t spread null into an array.
Even with the small bit of code we’re working with, this process has already gotten a bit annoying but with larger applications it can be more than just annoying. It can lead to repetitive, time consuming testing that takes multiple clicks and waiting in the browser, as well as missing some edge cases all together.
What if I didn’t have to wait to run the code to catch this? That’s where TypeScript comes in. Adding lang="ts"
to the component’s script section immediately reveals the error in my IDE and hovering over the error reminds me that the result of match could be an array but it could also be null.
With that information readily available we can now easily handle that edge case by defaulting to an empty array if the match returns null and then displaying the string “Invalid Phone” for our formatted phone number if match is undefined.
<script setup lang="ts">
import { ref, computed } from "vue";
const phone = ref("555666777777");
// default to empty array if result of .match is null
// (making match, one, two, and three undefined)
const [match, one, two, three] =
phone.value.match(/^(\d{3})(\d{3})(\d{4})$/) || [];
// 555-666-7777
const formatted = computed(() =>
// check if match is undefined
match ? ${one}-${two}-${three}
: "Invalid Phone"
);
</script>
<template>Phone: {{ formatted }}</template>
Furthermore, if we accidentally use a real number instead of a string again, TypeScript would alert us of that immediately as well.
Another benefit or working with TypeScript for your Vue.js projects, is that you can refactor with more confidence.
Let’s say you had a PostForm component for a blog application that looked something like this in plain JavaScript.
// PostForm.vue
<script setup>
import { ref } from "vue";
defineEmits(["create"]);
const title = ref("");
const body = ref("");
</script>
<template>
<form @submit.prevent="$emit('create', { body, title })">
<input v-model="title" />
<textarea v-model="body"></textarea>
<button>Create Post</button>
</form>
</template>
It keeps track of a title and a body as reactive references. It also emits a create event on form submit with an object that includes the body and title as properties.
Now let’s say you want to refactor that emit into it’s own handler function so that you can do more along with it (like do some validations, reset the form data after submit, or whatever).
// PostForm.vue
<script setup>
import { ref } from "vue";
defineEmits(["create"]);
const title = ref("");
const body = ref("");
const handleSubmit = () => {
$emit("create", { body, title });
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="title" />
<textarea v-model="body"></textarea>
<button>Create Post</button>
</form>
</template>
With ESLint enabled in my project, I do at least get notified here that $emit
is not defined.
To fix that we should capture the emit function returned from the defineEmits macro and then replace the emit inside of the handler function with it.
const emit = defineEmits(["create"]);
//...
const handleSubmit = () => {
emit("create", { body, title });
};
With that, as far as my IDE knows, everything is good to go. However there is actually still one more issue hiding in the refactor. Can you spot it?
<script setup>
import { ref } from "vue";
const emit = defineEmits(["create"]);
const title = ref("");
const body = ref("");
const handleSubmit = () => {
emit("create", { body, title });
};
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="title" />
<textarea v-model="body"></textarea>
<button>Create Post</button>
</form>
</template>
Don’t worry, if you didn’t. It’s tricky! The issue is that previously we were emitting the strings for the body and title. Now though, we are emitting the reactive refs since body and title are no longer being auto unwrapped.
Therefore, in order to keep our component interface the same as before the refactor, we should change the emit to include body.value
and title.value
.
emit("create", { body: body.value, title: title.value });
Whew! That was a close one. It’s definitely understandable how you might miss that in your own refactoring. The result unfortunately, would probably end up being minutes or more of wasted time trying to figure out what went wrong after testing things out in the browser.
Now let’s examine how the same refactor would probably play out in TypeScript.
First of all, if you were using TypeScript you’d be in the habit of typing your custom events. Thus, you’d already have defined the type of the create events payload something like this.
defineEmits<{
(e: "create", payload: { body: string; title: string }): void;
}>();
For the TypeScript novice, this just defines the type of the body and title properties in the payload object as strings.
Then after you handled the $emit
to emit
issue as before you’d end up with the following code.
<script setup lang="ts">
import { ref } from "vue";
const emit = defineEmits<{
(e: "create", payload: { body: string; title: string }): void;
}>();
const title = ref("");
const body = ref("");
const handleSubmit = () => {
emit("create", { body, title });
};
</script>
<template>
<form @submit="handleSubmit">
<input v-model="title" />
<textarea v-model="body"></textarea>
<button>Create Post</button>
</form>
</template>
This time, though, your IDE will let you know that something smells fishy. VS Code very directly calls out the body and title properties of the event payload with red squiggly lines. Hovering over each of them, shows the issue precisely: that is, a reactive ref is not the same thing as a string.
The solution, of course, is the same as before but this time we’re able to fix it immediately as the feedback was instant and in the context of the IDE. With the fix in place, the red squiggly lines disappear.
Besides the enhanced error visibility within your IDE when refactoring, TypeScript will also help you out when communicating between components. This is possible due to more focused and accurate auto-completion options and error detection for props and events.
Let’s take the same example from above. As a reminder, in JavaScript we’d write the emits definition like this.
// PostForm.vue
const emit = defineEmits(["create"]);
If you were to go and actually use the PostForm component in another component now and listen to the create event, you would get no autocomplete options for the event payload.
<PostForm @create="$event"/>
However, if you switch it back to the type declaration syntax for defining emits like this:
const emit = defineEmits<{
(e: "create", payload: { body: string; title: string }): void;
}>();
Then you would get a complete and extremely focused auto-complete list of the available properties on the event along with their types.
Just imagine how much easier this makes communicating between components! There’s no need whatsoever to go and checkout the code or docs for the component you’re consuming to see the payload of a particular event.
The same benefit applies to props as well. If we were to allow our PostForm to accept an existing post to edit that would look like this.
//PostForm.vue
const props = defineProps<{
// the question mark makes it an optional prop
// (since our form could be used for new or existing posts)
post?: { body: string; title: string };
}>();
//...
const title = ref(props.post?.title || "");
const body = ref(props.post?.body || "");
At this point, we could even normalize the shape of our post object into a reusable interface.
// PostForm.vue
interface Post {
body: string;
title: string;
}
const props = defineProps<{
post?: Post;
}>();
const emit = defineEmits<{
(e: "create", payload: Post): void;
}>();
Finally, we should see what actually passing the prop into the component looks like.
// App.vue
<script setup lang="ts">
import { ref } from "vue";
import PostForm from "@/components/PostForm.vue";
const existingPost = ref({
body: "",
tite: "",
});
</script>
<template>
<PostForm :post="existingPost" />
</template>
If you were paying close attention you might notice that I’ve got a typo in my existing post. If you were working in VS code directly though, you wouldn’t have to be paying close attention at all, because TypeScript would clearly call you out.
This is helpful not only for catching typos but, especially for larger objects, ensuring all necessary properties actually exist and are of the right type. In other words, you get documentation right in your IDE of exactly what a prop should look like.
For a JavaScript developer, TypeScript can be fairly intimidating. (I can say that because that was the case for me 😀.) However, jumping over the initial fear and learning even just the most basic syntaxes can help you start reaping the benefits of TypeScript in your Vue.js projects.
If you want to dive deeper into how to use TypeScript with your Vue.js projects, you should checkout our courses: TypeScript Fundamentals and TypeScript with Vue.js 3.
If you’ve never touched TypeScript or just need a refresher on the fundamentals, TypeScript Fundamentals will teach you the basic TypeScript principles and syntaxes that you’ll use on a regular basis.
TypeScript with Vue.js 3 is for those who already know the basics and teaches you the common use cases and ways you’ll use TypeScript in a typical Vue.js 3 project.
No matter where you’re at with TypeScript, if you can relate to the issues described in this article that TypeScript can help solve, then checkout our courses and as always we’re here to help! 😃
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.