In this article, you will learn how to create Custom Hooks with Vue 3 & Typescript.

TypeScript offers a static type system that can help to prevent many potential runtime errors as applications grow, this is why Vue 3 is built from the ground in TypeScript.

This means you don’t need any additional tooling to use TypeScript with Vue because it has full typescript support.


This article will take you on a step-by-step guide to creating a Vue 3 app with typescript support, adding TypeScript support to an existing Vue 3 app, and finally building Custom Hooks with Vue 3 & Typescript

Let’s start by creating a Vue 3 app with typescript support using the Vue-CLI tool.

Install vue-cli by executing the following command:

npm install --global @vue/cli

If you have the CLI already you can check if you have the right version:

vue --version

Make sure you upgrade it to the latest version with the following command:

npm update -g @vue/cli
# OR
yarn global upgrade --latest @vue/cli

Create a new Vue 3 app with TypeScript support

vue create vue-ts-project 

Choose the Manually select feature option
Hit the space key to select the following options:

  • choose Vue version
  • Babel
  • TypeScript
  • Linter / Formatter

Next, Choose Vue 3.x as the version for the project.

  • Enter yes to use class-style component syntax
  • Enter yes to use Babel alongside TypeScript
  • Select any linter of your choice

After the Vue-CLI successfully scaffolds the project, we will have a Vue3 project with full typescript support.

Execute the following command to serve the newly created project in the browser:

cd vue-ts-project
npm run serve

Adding TypeScript to an Existing Vue 3 Application

If you already have a Vue CLI project without TypeScript execute the following command to add typescript support:

vue add typescript

Make sure that script part of the component has TypeScript set as a language:

<script lang="ts">
  ...
</script>

Now you can write TypeScript in Vue components within your application.

Build Custom Hook with Vue 3 Class Component

Before we continue, the Vue official docs recommend using IDE such as VS code since it has great support for typescript.

With our Vue 3 Project completely set up, Let’s get into building a custom hook.
We will be building two composable hooks:

  1. The first hook will be used to interact directly with a rest API
  2. The second hook will depend on the first

Finally, we will add type safety to the project.

Create a folder called hooks with a file name api.ts in the src directory and add the code snippet below:

import { ref, Ref } from "vue";

export default function useApi (url, options){
  const response = ref();
  const request = async () => {
    const res = await fetch(url, options);
    const data = await res.json();
    response.value = data;
  };
  return { response, request };
}

Since our project is set up with typescript, some pieces of our code will be highlighted with red color signifying type errors.

Why do we have the error highlight?

If you hover on fetch, you will notice that it is a function that accepts two parameters the first parameter expects a type of RequestInfo while the second parameter expects an optional type of RequestInit or undefined:

function fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<Response>

Also hovering over the URL and option parameters in the useApi function you will notice that they currently have the type of any which does not match the type of the fetch function which they are using. Hence the typescript linter notifies with the error highlight.

(parameter) url: any
Parameter 'url' implicitly has an 'any' type.ts(7006)

Handling the Error

We will handle this error by defining the types for the URL and option parameters.

Custom Hooks with Vue 3

If you are using vs code, hovering on any of the highlighted parameters will suggest a quick fix that will automatically define the types for these parameters as follows:

import { ref } from "vue";
export default function useApi (url: RequestInfo, options?: RequestInit | undefined){
  const response = ref();
  const request = async () => {
    const res = await fetch(url, options);
    const data = await res.json();
    response.value = data;
  };
  return { response, request };
}

Add the ? symbol to the options parameter to notify the typeScript engine that the options parameter will be optional.

Next, we will create our second hook which will depend on the useApi hook to make request.

In the hooks folder, create a users.ts file with the following:

import useApi from "./api";
import { ref } from "vue";

export default async function useProducts(){
  const { response: products, request } = useApi(
    "https://ecomm-products.modus.workers.dev/"
  );
  const loaded = ref(false);
  if (loaded.value === false) {
    await request();
    loaded.value = true;
  }
  return { products };
}

we import the useApi hook from the api.ts file and ref from vue. ref
takes an inner value and returns a reactive and mutable ref object. The ref object has a single property .value that points to the inner value.
We pass in the API endpoint to useApi hook.

We destructured the response and request from the useApi hook for making API requests.

The ref object is used to set the initial value of loaded to false.

We renamed the response to product and finally return products as an object.

Use the Custom Hook

In the components folder create a Users.vue file and add the code below:

<template>
  <div>
    <h3>Customers</h3>
    <table id="customers" >
      <tr>
        <th>ID</th>
        <th>NAME</th>
        <th>USERNAME</th>
        <th>EMAIL</th>
        <th>ADDRESS</th>
        <th>PHONE</th>
        <th>WEBSITE</th>
      </tr>
      <tr v-for="user in users" :key="user.id">
        <td>{{user.id}}</td>
        <td>{{user.name}}</td>
        <td>{{user.username}}</td>
        <td>{{user.email}}</td>
        <td>{{user.address.street}}</td>
        <td>{{user.phone}}</td>
        <td>{{user.website}}</td>
      </tr>
    </table>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import useUsers from "@/hooks/users";
export default defineComponent({
  name: "Users",
  async setup() {
    const { users } = await useUsers();
    return { users };
  },
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#customers {
  font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
  border-collapse: collapse;
  width: 100%;
}
#customers td, #customers th {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}
#customers tr:nth-child(even){background-color: #f2f2f2;}
#customers tr:hover {background-color: #ddd;}
#customers th {
  padding-top: 12px;
  padding-bottom: 12px;
  text-align: left;
  background-color: #4CAF50;
  color: white;
}
</style>

The users component takes advantage of the composition API. Basically, this component fetches the list of users via the useUser hook and displays the users in the template.

Update App.vue with the following:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <Suspense>
      <template #default>
        <Users />
      </template>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </Suspense>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Users from "./components/Users.vue";
export default defineComponent({
  components: {
    Users
  }
});
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Vue 3 offers the Suspense component which will manage the asynchronous data fetching process, with a default view once the data is loading, and a fallback view when the data is being loaded all without the need for custom code.

The new Vue composition API will understand the current state of our component, so it will be able to differentiate if our component is loading or if it’s ready to be displayed.

Improving the Type of Safety

There is a need to define types for the API response data because response data could be unpredictable at times. So users.ts should be updated as follows:

import useApi from "./api";
import { Ref, ref } from "vue";
export interface Location {
  lat: number;
  lng: number;
}
export interface Address {
  street: string;
  suite: string;
  city: string;
  zipcode: number;
  geo: Location;
}
export interface User {
  id: string;
  name: string;
  username: string;
  email: string;
  address: Address;
}
export default async function useUserss() {
  const { response: users, request } = useApi<User[]>(
    "https://jsonplaceholder.typicode.com/users"
  );
  const loaded = ref(false);
  if (loaded.value === false) {
    await request();
    loaded.value = true;
  }
  return { users };
}

Conclusion

We have reached the end of this article, and we have learned how to set up vue 3 projects with typescript using the Vue-CLI tool, adding typescript to an existing vue 3 project, and finally build Custom Hooks with Vue 3 and typescript.