Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

TypeScript 在 Vue 項目中的完整實踐

在 Vue 項目裏用 TypeScript 其實比想象中麻煩一些。折騰了兩週,總結一下完整的配置流程。

為什麼 Vue + TS 配置複雜

Vue 2 的設計是基於選項對象的(Options API),不是 class 風格,對 TypeScript 的類型推斷不太友好。好在 Vue 提供了 vue-class-componentvue-property-decorator,讓 TS 支持好一些。

Vue 3 會在設計上對 TS 友好很多,但現在我們還在用 Vue 2。

項目配置

1. 初始化(Vue CLI 3)

bash
vue create my-ts-app
# 選擇 Manually select features
# 勾選 TypeScript, Babel, Router, Vuex, CSS Pre-processors, Linter
# TypeScript → Use class-style component syntax? → Yes

2. 依賴安裝

bash
npm install --save-dev \
  typescript \
  vue-class-component \
  vue-property-decorator \
  vuex-class

3. tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ESNext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": ["webpack-env", "jest"],
    "paths": {
      "@/*": ["src/*"]
    },
    "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"]
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules"]
}

組件寫法

Class 風格組件

typescript
{% raw %}
// src/components/UserProfile.vue
<template>
  <div class="user-profile">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
    <button @click="loadUser">刷新</button>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import { User } from '@/types'

@Component
export default class UserProfile extends Vue {
  @Prop({ required: true })
  userId!: number

  user: User | null = null
  loading = false

  async created() {
    await this.loadUser()
  }

  async loadUser() {
    this.loading = true
    try {
      this.user = await fetchUser(this.userId)
    } finally {
      this.loading = false
    }
  }

  get displayName(): string {
    return this.user?.name ?? '加載中...'
  }
}
</script>
{% endraw %}

類型定義

typescript
// src/types/index.ts

export interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  role: "admin" | "editor" | "viewer";
  createdAt: string;
}

export interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

export interface PaginatedData<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}

Vuex 的 TypeScript 支持

這是最麻煩的部分,需要 vuex-class

typescript
// src/store/modules/user.ts
import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators";
import { User } from "@/types";

@Module({ namespaced: true, name: "user" })
export default class UserModule extends VuexModule {
  currentUser: User | null = null;
  token = "";

  @Mutation
  SET_USER(user: User) {
    this.currentUser = user;
  }

  @Mutation
  SET_TOKEN(token: string) {
    this.token = token;
  }

  @Action({ rawError: true })
  async login(credentials: { username: string; password: string }) {
    const { user, token } = await authLogin(credentials);
    this.SET_USER(user);
    this.SET_TOKEN(token);
  }

  get isLoggedIn(): boolean {
    return !!this.token;
  }
}
typescript
// 在組件中使用
import { namespace } from "vuex-class";

const UserStore = namespace("user");

@Component
export default class App extends Vue {
  @UserStore.State("currentUser")
  currentUser!: User | null;

  @UserStore.Getter("isLoggedIn")
  isLoggedIn!: boolean;

  @UserStore.Action("login")
  login!: (credentials: {
    username: string;
    password: string;
  }) => Promise<void>;
}

API 請求的類型化

typescript
// src/api/user.ts
import axios from "axios";
import { User, ApiResponse, PaginatedData } from "@/types";

const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 10000,
});

export function fetchUser(id: number): Promise<User> {
  return request
    .get<ApiResponse<User>>(`/users/${id}`)
    .then((res) => res.data.data);
}

export function fetchUserList(params: {
  page: number;
  pageSize: number;
  keyword?: string;
}): Promise<PaginatedData<User>> {
  return request
    .get<ApiResponse<PaginatedData<User>>>("/users", { params })
    .then((res) => res.data.data);
}

export function updateUser(
  id: number,
  data: Partial<Omit<User, "id" | "createdAt">>,
): Promise<User> {
  return request
    .put<ApiResponse<User>>(`/users/${id}`, data)
    .then((res) => res.data.data);
}

遇到的問題

問題 1:.vue 文件無法識別

創建類型聲明文件:

typescript
// src/shims-vue.d.ts
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

問題 2:global properties 無類型提示

typescript
// src/shims-global.d.ts
import Vue from "vue";
import { AxiosInstance } from "axios";

declare module "vue/types/vue" {
  interface Vue {
    $http: AxiosInstance;
    $message: (msg: string) => void;
  }
}

問題 3:class-style 組件裏 this 的類型

Computed properties 和 methods 裏的 this 需要是組件實例類型:

typescript
// 在 methods 裏定義其他 methods 的類型
methods: {
  handleClick(this: ComponentType): void {
    this.someMethod()
  }
}

是否值得用 Vue + TS?

説實話,Vue 2 + TS 的體驗比 React + TS 差一些,裝飾器語法也還是提案階段(需要 experimentalDecorators: true)。類型推斷在某些 Vue 特有的場景(computed、watch)裏也不夠準確。

但對於大型項目,類型約束帶來的好處(減少低級錯誤、IDE 提示、重構安全性)還是值得的。

等 Vue 3 出來,Composition API + TS 的體驗應該會好很多。

小結

  • Vue CLI 3 初始化時選 TypeScript,自動處理大部分配置
  • 使用 vue-property-decorator 寫 class 風格組件
  • 類型定義放在 src/types/index.ts,全局複用
  • Vuex 用 vuex-module-decorators 獲得類型支持
  • Vue 2 + TS 體驗不完美,Vue 3 會改善

MIT Licensed