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. 你将学到


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 后:


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。

📖 小节


📝 作业

  1. 基础题(难度⭐) 将 1 个组件从 JS 改为 TS:

    • <script setup lang="ts">
    • defineProps 泛型
    • defineEmits 类型
    • 编译验证无错
  2. 进阶题(难度⭐⭐) 实现完整的 TS 类型系统:

    • 5 个 interface(User / Product / Order / Category / Cart)
    • 5 个组件用 defineProps 泛型
    • 5 个组件用 defineEmits 类型
    • tsconfig 严格模式
    • Volar 配置
  3. 挑战题(难度⭐⭐⭐) 实现完整的"Vue 3 + TS"项目:

    1. 5 个 store(Pinia + TS)
    2. 5 个 composable(TS)
    3. 10 个组件(泛型 + 类型推断)
    4. 5 大常见 TS 错误解决
    5. vue-tsc 类型检查(CI/CD)
    6. Volar + IDE 完美配置
    7. 类型文档自动生成(typedoc)
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏