Code前端首页关于Code前端联系我们

Vue3 搭配 TypeScript 该怎么学?从基础到实战的问答手册

terry 16小时前 阅读数 114 #SEO
文章标签 Vue3TypeScript

为什么 Vue3 要和 TypeScript 搭档?

前端项目越做越大后,“数据该是什么样”很容易混乱,TypeScript 就像给代码贴“身份标签”,写代码时提前抓错,不用等运行时才报错。

Vue3 对 TS 支持天生友好——它的源码是 TS 重写的,组合式 API(如 ref reactive)和 TS 类型系统无缝契合,举个例子:若组件接收“用户年龄” prop,用 JS 时传字符串要运行后才报错;用 TS 写,编辑器直接标红提醒“年龄得是数字”,从根上减少 Bug。

团队协作时,TS 让代码逻辑更透明,同事看一眼类型定义,就知道 props 该传啥、事件会触发什么数据;重构时改了类型,所有引用处自动提示,不用挨个排查漏改点。

怎么从零搭建 Vue3 + TypeScript 项目?

用 Vite 初始化最方便,命令行敲一行:

npm create vite@latest my-vue-ts-app -- --template vue-ts

进入项目后,重点看这几个核心文件:

  • tsconfig.json:TS 配置中心,建议开启 strict: true(强制空值、any 检查),配 baseUrl: "./src" 实现路径别名(如 @/components 代替 ../components)。

  • env.d.ts:给 .vue 文件做类型声明,Vite 自动生成的 declare module '*.vue' { ... },保证导入 .vue 文件时 TS 不报错。

  • vite.config.ts:Vite 配置文件,用 TS 写需导出 defineConfig,比如配置路径别名:

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import path from 'path'
    export default defineConfig({
      plugins: [vue()],
      resolve: {
        alias: {
          '@': path.resolve(__dirname, 'src')
        }
      }
    })

搭建完,在 src 下建 components views 等文件夹,就能写带 TS 的 Vue 组件啦~

Vue3 组件里怎么用 TypeScript 声明 Props?

选项式 API组合式 API(setup 语法糖),现在更推荐 setup 语法糖,写法更简洁。

组合式 API(<script setup lang="ts">

defineProps 配合 TS 泛型声明类型:

<script setup lang="ts">
// 方式1:内联类型
const props = defineProps<{ string; // 必传字符串
  size?: 'small' | 'medium' | 'large'; // 可选枚举
  isDisabled?: boolean; // 可选布尔
}>()
// 方式2:抽离接口(适合复杂类型)
interface ButtonProps { string;
  size?: 'small' | 'medium' | 'large';
  isDisabled?: boolean;
}
const props = defineProps<ButtonProps>()
// 加默认值用 withDefaults
const props = withDefaults(defineProps<ButtonProps>(), {
  size: 'medium',
  isDisabled: false
})
</script>

模板里用 props.title 能拿到类型提示,传参时写错类型编辑器直接报错。

选项式 API(defineComponent

适合习惯选项式的同学,用 defineComponent 包裹组件,Props 写在 props 选项:

import { defineComponent } from 'vue'
export default defineComponent({
  props: { {
      type: String,
      required: true
    },
    size: {
      type: String as () => 'small' | 'medium' | 'large', // 枚举需手动断言
      default: 'medium'
    },
    isDisabled: {
      type: Boolean,
      default: false
    }
  }
})

Ref 和 Reactive 的类型该怎么处理?

Vue3 响应式 API(ref reactive)和 TS 结合,关键是明确数据类型,避免推导出错。

Ref 的泛型

ref 是“包装器”,需通过泛型指定内部值类型:

// 初始值数字,类型推导为 Ref<number>
const count = ref(0) 
// 初始值 null,指定联合类型
const user = ref<User | null>(null) 
// 后续赋值需符合类型
count.value = 10 // ✅
count.value = 'ten' // ❌ TS 报错

存 DOM 元素时,指定 HTML 元素类型:

const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
  inputRef.value?.focus() // 可选链防止 null 报错
})

Reactive 的接口/类型别名

reactive 适合处理对象/数组,用接口或类型别名定义结构:

interface User {
  name: string;
  age: number;
  hobbies: string[];
}
const user = reactive<User>({
  name: '张三',
  age: 18,
  hobbies: ['篮球', '游戏']
})
// 数组的 reactive 同理
const list = reactive<string[]>([])
list.push('Vue3') // ✅
list.push(123) // ❌

组合式 API 抽离逻辑时怎么做好类型?

项目复杂后,重复逻辑会抽成 Composable(组合式函数),此时类型定义要清晰,方便复用。

例子:useCounter

需求:返回计数和自增方法,支持初始值。

// composables/useCounter.ts
import { ref, Ref } from 'vue'
interface CounterResult {
  count: Ref<number>;
  increment: (step?: number) => void;
}
export function useCounter(initialValue = 0): CounterResult {
  const count = ref(initialValue)
  const increment = (step = 1) => {
    count.value += step
  }
  return { count, increment }
}

组件里使用,TS 自动推导类型:

<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count, increment } = useCounter(5)
increment(2) // count 变为 7,step 自动提示 number 类型
</script>

泛型进阶:useFetch

接口请求场景,用泛型让返回数据类型更灵活:

// composables/useFetch.ts
import { ref, Ref } from 'vue'
import axios from 'axios'
interface ApiResponse<T> {
  code: number;
  data: T;
  msg: string;
}
export function useFetch<T>(url: string): Ref<ApiResponse<T> | null> {
  const data = ref<ApiResponse<T> | null>(null)
  axios.get<ApiResponse<T>>(url).then(res => {
    data.value = res.data
  })
  return data
}

组件调用时指定类型:

<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'
interface User {
  id: string;
  name: string;
}
const userData = useFetch<User>('/api/user/123')
// userData.value?.data 自动推导为 User 类型
</script>

自定义事件的类型怎么用 TypeScript 约束?

组件通信时,自定义事件(子组件 emit,父组件监听)的参数类型也能被 TS 约束,避免传错数据。

defineEmits 配合泛型或对象类型声明事件:

<script setup lang="ts">
// 方式1:对象类型(多事件场景)
const emit = defineEmits<{
  (event: 'change', value: number): void; // change 传 number
  (event: 'update', payload: { id: string; name: string }): void; // update 传对象
}>()
// 触发时参数类型不对会报错
emit('change', 'ten') // ❌ 应传 number
emit('update', { id: '1', name: '新名称' }) // ✅
// 方式2:内联泛型(简单事件场景)
const emit = defineEmits<(event: 'submit', data: string) => void>()
</script>

父组件监听时,事件处理函数参数类型自动推导:

<MyComponent 
  @change="handleChange" 
  @update="handleUpdate"
/>
<script setup lang="ts">
const handleChange = (value: number) => {
  // value 自动是 number 类型
}
const handleUpdate = (payload: { id: string; name: string }) => {
  // payload 结构清晰
}
</script>

Vue Router 和 TypeScript 怎么结合更丝滑?

Vue Router 4 对 TS 支持友好,重点是路由配置的类型扩展动态参数的类型推导

路由配置的 Meta 类型

env.d.ts 扩展 RouteMeta 接口,给路由元信息加类型:

// env.d.ts
import 'vue-router'
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean; // 是否需登录: string; // 页面标题
  }
}

路由配置(router.ts)里用:

import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
      meta: { title: '首页' } // 自动提示 title 是 string
    },
    {
      path: '/profile',
      name: 'Profile',
      component: () => import('@/views/Profile.vue'),
      meta: { requiresAuth: true, title: '个人中心' } // 类型匹配
    }
  ]
})

动态路由参数

如路由 path: '/user/:id'useRoute() 返回的 route.params.id 类型是 string,TS 自动推导:

<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = route.params.id // 自动是 string 类型
</script>

Pinia 状态管理怎么发挥 TypeScript 的优势?

Pinia 是 Vue 官方状态管理库,天生支持 TS 类型推导,无需额外声明也能有提示。

定义 Store

// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    user: { name: '匿名', age: 0 } as { name: string; age: number } // 也可抽离接口
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment(step = 1) {
      this.count += step
    }
  }
})

组件里使用

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
store.count // 自动是 number 类型
store.doubleCount // 自动是 number 类型
store.increment(2) // step 自动是 number 类型
</script>

封装 Axios 请求时怎么用 TypeScript 规范类型?

后端接口数据结构复杂,用 TS 给请求和响应加类型,避免“拿到数据不知咋用”。

步骤1:定义接口响应结构

// types/api.ts
export interface ApiResponse<T> {
  code: number;
  data: T;
  msg: string;
}
export interface User {
  id: string;
  name: string;
  age: number;
}

步骤2:封装请求函数

// utils/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
const instance: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASEURL,
  timeout: 5000
})
// 请求拦截器
instance.interceptors.request.use((config: AxiosRequestConfig) => {
  // 加 token 等逻辑
  return config
})
// 响应拦截器
instance.interceptors.response.use((response: AxiosResponse<ApiResponse<any>>) => {
  if (response.data.code !== 200) {
    // 全局错误处理
  }
  return response.data
})
// 封装 GET
export function get<T>(url: string, params?: any): Promise<ApiResponse<T>> {
  return instance.get(url, { params })
}
// 封装 POST
export function post<T>(url: string, data?: any): Promise<ApiResponse<T>> {
  return instance.post(url, data)
}

步骤3:组件里调用

<script setup lang="ts">
import { get } from '@/utils/request'
import { User } from '@/types/api'
const fetchUser = async () => {
  const res = await get<User>('/user/123')
  // res.data 自动是 User 类型,能点出 id、name、age
  console.log(res.data.name)
}
</script>

实战:写个带完整类型的表单组件要注意什么?

登录表单组件为例,需求:接收默认用户名(可选)、包含用户名/密码/记住我、提交时 emit 表单数据。

步骤1:定义 Props

<script setup lang="ts">
interface LoginProps {
  defaultUser?: string; // 可选默认用户名
}
const props = withDefaults(defineProps<LoginProps>(), {
  defaultUser: ''
})
</script>

步骤2:定义表单数据的 Ref

const user = ref<string>(props.defaultUser)
const pwd = ref<string>('')
const remember = ref<boolean>(false)

步骤3:定义自定义事件

const emit = defineEmits<{
  (event: 'submit', data: { user: string; pwd: string; remember: boolean }): void;
}>()

步骤4:表单提交方法

const handleSubmit = () => {
  if (!user.value || !pwd.value) {
    alert('请填写用户名和密码')
    return
  }
  emit('submit', {
    user: user.value,
    pwd: pwd.value,
    remember: remember.value
  })
}

步骤5:模板编写

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="user" placeholder="用户名" />
    <input v-model="pwd" type="password" placeholder="密码" />
    <label>
      <input type="checkbox" v-model="remember" /> 记住我
    </label>
    <button type="submit">登录</button>
  </form>
</template>

父组件使用

<template>
  <LoginForm 
    @submit="handleLogin" 
    default-user="admin"
  />
</template>
<script setup lang="ts">
const handleLogin = (data: { user: string; pwd: string; remember: boolean }) => {
  // data.user 自动是 string 类型,直接用
  console.log('提交的用户名:', data.user)
}
</script>

遇到 TypeScript 类型报错怎么快速解决?

写代码时遇到 TS 红色波浪线别慌,常见场景有套路:

场景1:“Object is possibly 'null' or 'undefined'”

原因:TS 检测到变量可能为 null/undefined,直接访问属性会报错。
解决:

  • 可选链()obj?.prop,obj 为 null/undefined 时返回 undefined,不报错。
  • 非空断言()obj!.prop,告诉 TS “我保证这里非空”(谨慎用,确保运行时真有值)。
  • 类型守卫if (obj) { obj.prop },判断后再访问。

例子:

const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
  inputRef.value?.focus() // 可选链 ✅
  inputRef.value!.focus() // 非空断言 ✅(需确保值存在)
  if (inputRef.value) inputRef.value.focus() // 类型守卫 ✅
})

场景2:“Type 'X' is not assignable to type 'Y'”

原因:赋值类型与声明类型不匹配。
解决:检查变量声明和**

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门