Vue.js: TypeScript 最佳实践
最后更新:2026-08-26
TypeScript 是 Vue 3 企业级项目的标配——它提供类型安全、IDE 智能补全、重构信心。Vue 3.4+ 的 <script setup lang="ts"> 让 TS 集成达到新高度:defineProps / defineEmits 自动推断、组件 ref 类型完美。
掌握 Vue + TypeScript 是从初学者到高级开发的关键门槛。本课帮你建立 Vue 3 + TS 的完整知识体系。
1. 你将学到
<script setup lang="ts">基础- defineProps 泛型 5 种写法
- defineEmits 类型 + 参数约束
- ref / reactive / computed 类型推断
- 组件 ref 类型(useTemplateRef)
- Volar 扩展配置
- tsconfig.json 最佳实践
- 5 个常见 TS 错误
2. 一个 JS 项目的"undefined is not a function"噩梦
(1) 痛点:JS 项目中 100 个 undefined 错误
Alice 的后台原本是 JS 项目。常见 Bug:
JS
// ❌ 翻车版:JS 错误直到运行时才发现
export default {
props: {
user: { type: Object, required: true }
// 拼错 prop 名:userName(不报错)
// prop 类型错了:不报错
// emit 事件名错了:不报错
}
}
VUE
<!-- 父组件用了 userName,但子组件定义的是 user -->
<UserCard userName="Alice" /> <!-- ❌ 运行时才发现 -->
<!-- emit 事件名错了 -->
<Child @updae="handler" /> <!-- ❌ 运行时才发现 -->
100+ 个潜在 Bug 都在运行时才暴露,调试成本高。
(2) Vue 3 + TypeScript 解法
VUE
<!-- 子组件:UserCard.vue -->
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
user: User
variant?: 'primary' | 'secondary'
}>()
const emit = defineEmits<{
select: [userId: number]
delete: [userId: number]
}>()
</script>
VUE
<!-- 父组件使用:编译时就报错 -->
<UserCard :user="alice" /> <!-- ✅ 编译时类型检查 -->
<UserCard @updae="handler" /> <!-- ❌ TS 报错:事件不存在 -->
所有错误在编译时发现,IDE 直接划红线。
(3) 收益
加上 TypeScript 后:
- 运行时错误:80% → 10%(编译时拦截)
- IDE 补全:准确率 95%+
- 重构信心:类型保护,不怕改坏
- 团队协作:接口明确,沟通成本低
3. TypeScript 基础配置
(1) script setup lang="ts"
VUE
<template>
<p>{{ count }}</p>
<button @click="increment">+</button>
</template>
<script setup lang="ts">
const { ref } = Vue
// ✅ TS 自动推断:Ref<number>
const count = ref(0)
// ✅ 参数和返回值类型
function increment(): void {
count.value++
}
</script>
(2) tsconfig.json 基础
JSON
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}
(3) 5 大推荐配置
JSON
{
"compilerOptions": {
// 1. 严格模式(必开)
"strict": true,
// 2. 无隐式 any
"noImplicitAny": true,
// 3. 严格空检查
"strictNullChecks": true,
// 4. 严格函数类型
"strictFunctionTypes": true,
// 5. 严格绑定调用
"strictBindCallApply": true
}
}
4. defineProps 泛型 5 种写法
(1) 写法 1:基础类型
TS
const props = defineProps<{
name: string
age: number
active: boolean
}>()
(2) 写法 2:可选 + 默认值
TS
// withDefaults 提供默认值
const props = withDefaults(defineProps<{
name: string
age?: number
variant?: 'primary' | 'secondary'
}>(), {
age: 18,
variant: 'primary'
})
(3) 写法 3:复杂对象 / 数组
TS
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
user: User
items: User[]
config: Record<string, unknown>
}>()
(4) 写法 4:函数 prop
TS
const props = defineProps<{
formatter: (value: number) => string
onChange: (value: string) => void
}>()
(5) 写法 5:泛型组件
TS
// 泛型组件:List<T> 可指定 item 类型
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
items: T[]
selected?: T
}>()
</script>
<!-- 使用 -->
<List :items="users" /> <!-- T = User -->
<List :items="products" /> <!-- T = Product -->
5. defineEmits 类型
(1) 5 种事件声明
TS
// 1. 简单事件
const emit = defineEmits<{
click: []
submit: []
}>()
// 2. 单参数
const emit = defineEmits<{
select: [id: number]
delete: [id: number]
}>()
// 3. 多参数
const emit = defineEmits<{
change: [id: number, oldValue: string, newValue: string]
}>()
// 4. 可选参数
const emit = defineEmits<{
search: [query?: string]
load: [id: number, options?: object]
}>()
// 5. void 返回值
const emit = defineEmits<{
success: [data: object]
error: [message: string]
}>()
(2) 触发事件
TS
const emit = defineEmits<{
select: [id: number]
delete: [id: number]
}>()
// ✅ TypeScript 检查参数类型
emit('select', 123) // ✅ OK
emit('select', 'abc') // ❌ TS 错误
emit('delete', 456)
(3) 父组件使用
VUE
<template>
<!-- ✅ TS 检查事件名 -->
<Child @select="handleSelect" @delete="handleDelete" />
<!-- ❌ TS 错误:事件不存在 -->
<!-- <Child @updae="handler" /> -->
</template>
<script setup lang="ts">
function handleSelect(id: number) {
console.log('Selected:', id)
}
</script>
6. ref / reactive / computed 类型
(1) ref 类型推断
TS
const { ref } = Vue
// 自动推断为 Ref<number>
const count = ref(0)
count.value = 1 // ✅
// 推断为 Ref<string>
const name = ref('Alice')
// 推断为 Ref<number | undefined>(可能是 undefined)
const maybeNumber = ref<number>()
maybeNumber.value // type: number | undefined
(2) reactive 类型推断
TS
const { reactive } = Vue
// 自动推断
const state = reactive({
count: 0,
user: { name: 'Alice', age: 25 }
})
state.count // type: number
state.user.name // type: string
// 显式类型
interface State {
count: number
items: string[]
}
const s = reactive<State>({
count: 0,
items: []
})
(3) computed 类型
TS
const { ref, computed } = Vue
const count = ref(10)
// 自动推断:ComputedRef<number>
const double = computed(() => count.value * 2)
// 显式类型
const formatted = computed<string>(() => `Count: ${count.value}`)
7. 组件 ref 类型(useTemplateRef)
(1) Vue 3.5+ useTemplateRef
VUE
<template>
<input ref="usernameInput">
<MyChart ref="chartComponent" :data="chartData" />
</template>
<script setup lang="ts">
const { useTemplateRef, onMounted } = Vue
import MyChart from './MyChart.vue'
// ✅ TS 自动推断:Ref<HTMLInputElement | null>
const inputRef = useTemplateRef<HTMLInputElement>('usernameInput')
// ✅ 组件实例类型
const chartRef = useTemplateRef<InstanceType<typeof MyChart>>('chartComponent')
onMounted(() => {
inputRef.value?.focus() // TS 自动补全
chartRef.value?.refresh()
})
</script>
(2) Vue 3.4- 旧写法
TS
// 旧写法:需要手动 ref<>
const { ref, onMounted } = Vue
import MyChart from './MyChart.vue'
const inputRef = ref<HTMLInputElement | null>(null)
const chartRef = ref<InstanceType<typeof MyChart> | null>(null)
8. Volar 扩展配置
(1) 安装
BASH
# VS Code 装 "Vue - Official" 扩展(Volar)
# 搜索:Vue - Official
(2) 推荐 settings.json
JSON
{
"vue.enabled.volar": true,
"vue.compilerOptions.target": 3.4,
"vue.complete.casing.tags": ["PascalCase", "snake_case"],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.includePackageJsonAutoImports": "on",
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "Vue.volar"
}
}
(3) 5 大 Volar 功能
| 功能 | 说明 |
|---|---|
| 类型推断 | props / emits / ref 完美推断 |
| 自动补全 | 组件名 / props / emits 智能提示 |
| 错误检查 | 编译时显示错误(如 prop 类型错) |
| 跳转到定义 | F12 跳到组件定义 |
| 重构支持 | 重命名 prop 自动同步所有引用 |
9. 完整示例:5 大 TS 模式
▶ 示例:defineProps + defineEmits TypeScript 写法(⚠️ 需 Vite + ts)
⚠️ 以下代码需在 Vite + TypeScript 项目中运行,CDN 全局构建不支持 TS 编译。展示核心 API:
TS
// ✅ defineProps 5 种写法
// 1. 基础
defineProps<{ name: string }>()
// 2. 可选+默认
withDefaults(defineProps<{ name?: string }>(), { name: 'Guest' })
// 3. 复杂
defineProps<{ user: User; items: User[] }>()
// 4. 函数
defineProps<{ onClick: () => void }>()
// 5. 泛型(需 generic="T")
defineProps<{ items: T[] }>()
TS
// ✅ defineEmits 5 种事件
// 1. 简单
defineEmits<{ click: [] }>()
// 2. 单参
defineEmits<{ select: [id: number] }>()
// 3. 多参
defineEmits<{ change: [old: string, new: string] }>()
// 4. 可选
defineEmits<{ search: [q?: string] }>()
// 5. void
defineEmits<{ done: [] }>()
TS
// ✅ ref<T> + 类型推导
import { ref } from 'vue'
const count = ref(0) // 自动推导 Ref<number>
const user = ref<User | null>(null)
▶ 示例:CDN Vue 3 + 类型推导演示
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>count: {{ state.count }} | user: {{ state.user || '(空)' }}</p>
<button @click="increment">+1</button>
</div>
<script>
// JSDoc 注释提供类型提示
const { createApp, ref, reactive } = Vue
const App = {
setup() {
/** @type {{ count: number, user: string | null }} */
const state = reactive({ count: 0, user: null })
function increment() { state.count++ }
return { state, increment }
}
}
createApp(App).mount('#app')
</script>
注:CDN 环境用 JSDoc 注释获得类型提示。生产用 Vite + tsconfig.json +
<script setup lang="ts">。
▶ 示例:5 个常见 TS 错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
Property 'x' does not exist |
拼错或缺类型 | 检查拼写,添加类型 |
Argument of type 'X' is not assignable |
类型不匹配 | 检查参数类型 |
Type 'X' is not assignable to type 'Y | null |
严格空检查 | 添加 ! 或 ? |
Cannot find module './X' |
路径错误 | 检查导入 |
Object is possibly 'undefined' |
可选值 | 用 ?. 可选链 |
▶ 示例:5 大性能对比
| 模式 | 类型安全 | 性能 | 适用 |
|---|---|---|---|
| JS | ❌ | ⭐⭐⭐⭐⭐ | 原型/小项目 |
| TS(基础) | ⭐⭐⭐ | ⭐⭐⭐⭐ | 通用 |
| TS(严格) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 企业项目 |
| TS + Volar | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 推荐 |
| TS + tsc build | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大型项目 |
▶ 示例:5 大 Vue 3 + TS 项目结构
TEXT
📖 仅展示
src/
├── components/ # 公共组件
│ ├── UserCard.vue # <script setup lang="ts">
│ └── BaseButton.vue
├── views/ # 页面组件
├── stores/ # Pinia(TS)
├── composables/ # Composables
├── types/ # 类型定义
│ ├── user.ts # export interface User
│ ├── api.ts # export interface ApiResponse<T>
│ └── index.ts # 统一导出
├── utils/ # 工具函数
├── router/ # 路由(TS)
├── App.vue
└── main.ts # 入口
▶ 示例:5 大常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 拼错 prop 名 | 编译报错 | 改用 IDE 跳转定义 |
| 拼错 emit 名 | 编译报错 | 用 defineEmits 类型 |
| 类型转换失败 | 编译报错 | 用类型断言或泛型 |
| ref.value undefined | 运行时 | 用可选链 ?. |
| 类型循环引用 | 编译错误 | 用 type-only import |
❓ 常见问题
Q Vue 3 + TS 性能比 JS 差吗?
A 编译时类型检查,运行时无影响(类型被擦除)。Volar 编译速度很快,几乎无感知。
Q defineProps 泛型 vs 运行时声明?
A 推荐泛型。TypeScript 自动推断 props 类型,IDE 完美补全。运行时声明需要手写 validator。
Q 泛型组件怎么写?
A
<script setup lang="ts" generic="T">,然后 defineProps<{ items: T[] }>()。Vue 3.3+ 支持。Q defineModel 在 TS 中怎么写?
A
const modelValue = defineModel<string>('modelValue', { default: '' })。泛型指定类型。Q useTemplateRef 必须在 Vue 3.5+ 吗?
A 是的。Vue 3.4 及以下用
ref<HTMLInputElement | null>(null)。Q tsconfig.json 的 strict 模式必开吗?
A 企业项目必开。新手可以先关 strict 慢慢加。推荐:
"strict": true, "noImplicitAny": true, "strictNullChecks": true。Q Volcano / vue-tsc 是什么?
A vue-tsc 是 Vue 项目的 TypeScript 检查工具(Volar 团队开发)。
npx vue-tsc --noEmit 跑类型检查(不输出 JS,只检查类型)。Q Vue 3 + TS 在 SSR 中能用吗?
A 能。Nuxt 3 内置 TypeScript 支持。Pinia / Vue Router 都原生支持 TS。
📖 小节
<script setup lang="ts">是 Vue 3 + TS 的标配- defineProps 泛型 5 种写法:基础 / 可选 / 复杂 / 函数 / 泛型
- defineEmits 类型 + 参数约束保证事件正确
- 5 大推荐 tsconfig:strict / noImplicitAny / strictNullChecks 等
- useTemplateRef(Vue 3.5+)完美类型推断
- Volar 是 VS Code 必备扩展
- 5 大常见错误:拼写 / 类型 / 循环引用
📝 作业
-
基础题(难度⭐) 将 1 个组件从 JS 改为 TS:
<script setup lang="ts">- defineProps 泛型
- defineEmits 类型
- 编译验证无错
-
进阶题(难度⭐⭐) 实现完整的 TS 类型系统:
- 5 个 interface(User / Product / Order / Category / Cart)
- 5 个组件用 defineProps 泛型
- 5 个组件用 defineEmits 类型
- tsconfig 严格模式
- Volar 配置
-
挑战题(难度⭐⭐⭐) 实现完整的"Vue 3 + TS"项目:
- 5 个 store(Pinia + TS)
- 5 个 composable(TS)
- 10 个组件(泛型 + 类型推断)
- 5 大常见 TS 错误解决
- vue-tsc 类型检查(CI/CD)
- Volar + IDE 完美配置
- 类型文档自动生成(typedoc)