Vue3 搭配 TypeScript 该怎么学?从基础到实战的问答手册
为什么 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前端网发表,如需转载,请注明页面地址。
code前端网



