DEV Community

Delia
Delia

Posted on

Using Vue.js with TypeScript: Boost Your Code Quality

Vue.js and TypeScript are a powerful combination for building robust, maintainable, and scalable web applications. TypeScript adds static typing to JavaScript, helping you catch errors early and improve your code quality. In this guide, we'll explore how to use Vue.js with TypeScript, focusing on the Composition API. We'll provide detailed, beginner-friendly explanations, tips, and best practices to help you get started and advance your skills.

Why Use TypeScript with Vue.js?

Advantages

  1. Early Error Detection: TypeScript helps catch errors during development, reducing runtime bugs.
  2. Improved Code Quality: Static types make your code more predictable and easier to understand.
  3. Better Tooling: Enhanced IDE support with features like autocompletion, refactoring, and navigation.
  4. Scalability: Makes large codebases easier to maintain and refactor.

Errors We Can Miss Without TypeScript

Without TypeScript, you might miss common errors such as:

  • Type Errors: Passing a string when a number is expected.
  • Property Errors: Accessing properties that don't exist on an object.
  • Function Signature Errors: Incorrect arguments passed to functions.
  • Refactoring Issues: Changes that break parts of the codebase without immediate detection.

Before and After TypeScript

Before TypeScript

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "Hello, Vue!"
    };
  },
  methods: {
    updateMessage(newMessage) {
      this.message = newMessage;
    }
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

After TypeScript

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const message = ref<string>("Hello, Vue!");

    function updateMessage(newMessage: string): void {
      message.value = newMessage;
    }

    return {
      message,
      updateMessage
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Benefits of Using TypeScript

  • Type Safety: Ensures you’re using variables and functions as intended.
  • Documentation: Types serve as documentation for your code, making it easier for others (or future you) to understand.
  • Refactoring: Easier and safer refactoring with TypeScript's type-checking.
  • Tooling: Better IDE support with autocompletion and inline error detection.

Setting Up Vue.js with TypeScript

Prerequisites

  • Node.js installed
  • Basic understanding of Vue.js and JavaScript

Step 1: Create a Vue Project with TypeScript

Use Vue CLI to create a new project with TypeScript support:

npm install -g @vue/cli
vue create my-vue-typescript-app
Enter fullscreen mode Exit fullscreen mode

Select Manually select features and choose TypeScript from the list of features.

Step 2: Install Dependencies

If you’re adding TypeScript to an existing Vue project, install the necessary dependencies:

npm install typescript @vue/cli-plugin-typescript --save-dev
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure TypeScript

Ensure you have a tsconfig.json file in your project root. This file configures the TypeScript compiler options:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"]
}
Enter fullscreen mode Exit fullscreen mode

Using the Composition API with TypeScript

The Composition API provides a more flexible and reusable way to manage component logic. Let's explore how to use it with TypeScript.

Basic Example: Counter Component

Step 1: Define the Component

Create a new Vue component using TypeScript and the Composition API.

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const count = ref<number>(0);

    function increment(): void {
      count.value++;
    }

    return {
      count,
      increment
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Detailed Explanation

  • defineComponent: Helps TypeScript understand the Vue component's structure.
  • ref: Declares a reactive variable count of type number.
  • setup: The setup function is the entry point for using the Composition API. It returns the variables and functions to the template.

Advanced Example: Fetching Data

Let's create a more complex example where we fetch data from an API.

Step 1: Define the Component

Create a new component to fetch and display data.

<template>
  <div>
    <p v-if="loading">Loading...</p>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';

interface User {
  id: number;
  name: string;
}

export default defineComponent({
  setup() {
    const users = ref<User[]>([]);
    const loading = ref<boolean>(true);

    async function fetchUsers() {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        users.value = await response.json();
      } catch (error) {
        console.error('Error fetching users:', error);
      } finally {
        loading.value = false;
      }
    }

    onMounted(fetchUsers);

    return {
      users,
      loading
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Detailed Explanation

  • Interface User: Defines the shape of the user data.
  • ref: Declares a reactive array of users.
  • onMounted: Lifecycle hook that runs fetchUsers when the component is mounted.

Benefits of Using TypeScript with Fetch Example

  • Type Safety: Ensures the fetched data conforms to the expected structure.
  • Autocompletion: Provides better autocompletion and navigation within IDEs.
  • Error Handling: TypeScript can help catch errors related to data handling early.

Additional Examples and Benefits

Form Handling with TypeScript

Before TypeScript

<template>
  <form @submit.prevent="submitForm">
    <input v-model="name" placeholder="Name" />
    <button type="submit">Submit</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      name: ''
    };
  },
  methods: {
    submitForm() {
      console.log(this.name);
    }
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

After TypeScript

<template>
  <form @submit.prevent="submitForm">
    <input v-model="name" placeholder="Name" />
    <button type="submit">Submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    const name = ref<string>('');

    function submitForm(): void {
      console.log(name.value);
    }

    return {
      name,
      submitForm
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Benefits of TypeScript with Form Handling

  • Predictable State: Ensures name is always a string.
  • Refactoring Safety: Easier to refactor with confidence that type errors will be caught.

Using Interfaces for Props

Before TypeScript

<template>
  <div>{{ user.name }}</div>
</template>

<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    }
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

After TypeScript

<template>
  <div>{{ user.name }}</div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

interface User {
  name: string;
}

export default defineComponent({
  props: {
    user: {
      type: Object as PropType<User>,
      required: true
    }
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Benefits of Using Interfaces for Props

  • Type Checking: Ensures the prop passed to the component matches the expected type.
  • Documentation: Serves as documentation for what the component expects.

Tips and Best Practices

1. Use Type Annotations

Always use type annotations to make your code more readable and maintainable.

const count = ref<number>(0);
const users = ref<User[]>([]);
Enter fullscreen mode Exit fullscreen mode

2. Utilize Interfaces

Define interfaces for complex data structures to ensure type safety.

interface User {
  id: number;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

3. Enable Strict Mode

Enable TypeScript's strict mode to catch more potential errors and enforce best practices.

"strict": true
Enter fullscreen mode Exit fullscreen mode

4. Use defineComponent

Wrap your component definition with defineComponent for better type inference.

import { defineComponent } from 'vue';

export default defineComponent({
  // component options
});
Enter fullscreen mode Exit fullscreen mode

5. Handle Asynchronous Code

Use async/await for handling asynchronous operations and add appropriate error handling.

async function fetchUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    users.value = await response.json();
  } catch (error) {
    console.error('Error fetching users:', error);
  } finally {
    loading.value = false;
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Avoid Using any

Avoid using the any type as much as possible. It defeats the purpose of using TypeScript by allowing any type of data.

Additional Examples

Using watch with TypeScript

The watch function is used to monitor reactive data and perform side effects.

Example: Watching a Reactive Property

<template>
  <div>
    <input v-model="name" placeholder="Enter your name" />
    <p>{{ message }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from 'vue';

export default defineComponent({
  setup() {
    const name = ref<string>('');
    const message = ref<string>('Hello!');

    watch(name, (newValue, oldValue) => {
      if (newValue) {
        message.value = `Hello, ${newValue}!`;
      } else {
        message.value = 'Hello!';
      }
    });

    return {
      name,
      message
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Benefits of Using watch with TypeScript

  • Type Safety: Ensures the values being watched and the callback parameters conform to expected types.
  • Predictability: Reduces runtime errors by catching type-related issues during development.

Using computed with TypeScript

The computed function allows you to create reactive derived state.

Example: Using computed Properties

<template>
  <div>
    <input v-model="firstName" placeholder="First Name" />
    <input v-model="lastName" placeholder="Last Name" />
    <p>Full Name: {{ fullName }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';

export default defineComponent({
  setup() {
    const firstName = ref<string>('');
    const lastName = ref<string>('');

    const fullName = computed<string>(() => {
      return `${firstName.value} ${lastName.value}`;
    });

    return {
      firstName,
      lastName,
      fullName
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Benefits of Using computed with TypeScript

  • Type Safety: Ensures the computed property returns the correct type.
  • Readability: Makes it clear what derived state depends on, improving code readability.

Handling Events with TypeScript

Example: Typed Event Handlers

<template>
  <div>
    <button @click="handleClick">Click Me</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  setup() {
    function handleClick(event: MouseEvent): void {
      console.log('Button clicked', event);
    }

    return {
      handleClick
    };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Benefits of Typed Event Handlers

  • Type Safety: Ensures the event parameter is correctly typed.
  • Autocompletion: Provides better autocompletion and documentation in your IDE.

Using Vue.js with TypeScript, especially with the Composition API, enhances your code quality, maintainability, and development experience. By following these tips and best practices, you can create robust, type-safe Vue applications that are easy to understand and maintain. Start incorporating TypeScript into your Vue projects today and enjoy the benefits of improved tooling, early error detection, and scalable code architecture. Happy coding!

Top comments (0)