同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、开篇:Vue3 为什么值得上 TypeScript?
日常开发里常会遇到:
- 重构不敢动:改了 props 名字,不知道有没有地方漏改
- 调用传参靠猜:emit 的事件参数类型不清楚,写完才发现传错
- ref / reactive 混用:什么时候用泛型、什么时候用
as,心里没数
在 Vue3 + TS 里,把这些都加上类型注解,既能获得智能提示,也能在编译期就发现错误,减少线上 bug。
下面从基础概念 → 具体用法 → 完整示例 → 选型与踩坑,把 setup、props、emit、ref、reactive 的类型写法讲清楚。
二、概念扫盲:Vue3 里的类型体系
先把几个核心类型分清:
| 概念 | 作用 |
|---|---|
| defineProps | 定义组件的 props 及类型 |
| defineEmits | 定义组件触发的自定义事件及参数类型 |
| ref | 基本类型/单值响应式 |
| reactive | 对象响应式 |
| computed | 计算属性 |
| watch | 监听器 |
一句话记:props 和 emit 用 defineXxx 配合类型,ref/reactive 用泛型或 as 标注类型。
三、setup 与 <script setup> 的关系
Vue3 推荐用 <script setup>,和传统 setup 的对应关系是:
<!-- 传统 setup 写法 -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, { emit }) {
// ...
}
})
</script>
<!-- 推荐:<script setup> -->
<script setup lang="ts">
// props、emit 等由编译器宏自动推导,无需显式返回
</script>
<script setup> 下,defineProps 和 defineEmits 是编译器宏,会在编译时处理,无需 return。
四、props 的类型注解
4.1 两种写法
方式一:运行时声明(同时满足 TS 类型)
<script setup lang="ts">
const props = defineProps<{
title: string
count?: number
tags: string[]
}>()
// 使用
console.log(props.title) // string
console.log(props.count) // number | undefined
</script>
方式二:用接口抽离(更易复用)
<script setup lang="ts">
interface Props {
title: string
count?: number
tags: string[]
}
const props = defineProps<Props>()
</script>
4.2 带默认值的 props
泛型写法本身不直接支持 default,需要用 withDefaults:
<script setup lang="ts">
interface Props {
title: string
count?: number
tags?: string[]
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
tags: () => ['default']
})
</script>
注意:tags 用工厂函数 () => ['default'],避免数组在多个实例间共享引用。
4.3 解构 props 会丢响应式
// ❌ 解构后不再是响应式
const { title } = defineProps<{ title: string }>()
// ✅ 先完整拿到 props,再解构或使用
const props = defineProps<{ title: string }>()
const title = computed(() => props.title)
五、emit 的类型注解
5.1 基本写法
<script setup lang="ts">
// 写法一:对象形式,带校验
const emit = defineEmits<{
change: [value: string]
update: [id: number, name: string]
}>()
// 调用
emit('change', 'hello')
emit('update', 1, 'vue')
</script>
[value: string] 表示该事件接收一个 string 参数,[id: number, name: string] 表示两个参数。
5.2 可选参数
const emit = defineEmits<{
submit: [payload?: { name: string; age: number }]
}>()
emit('submit') // ✅
emit('submit', { name: 'Tom', age: 18 }) // ✅
5.3 常见误用
// ❌ 事件名写错,类型检查不会报错
emit('chang', 'hello') // 应该是 'change'
// ✅ 建议统一用对象形式,方便以后扩展校验
const emit = defineEmits<{
change: [value: string]
}>()
六、ref 的类型注解
6.1 基本类型
const count = ref(0) // 自动推断为 Ref<number>
const name = ref<string>('') // 显式指定 Ref<string>
const list = ref<string[]>([]) // 数组
6.2 对象类型
interface User {
id: number
name: string
}
// 方式一:泛型
const user = ref<User | null>(null)
// 方式二:as(在初始化时断言)
const user = ref(null) as Ref<User | null>
6.3 取值时注意 .value
const count = ref(0)
count.value++ // ✅
const n = count.value // ✅
// 模板里会自动解包,不需要 .value
// {{ count }}
七、reactive 的类型注解
7.1 基本用法
interface FormState {
name: string
age: number
}
const form = reactive<FormState>({
name: '',
age: 0
})
7.2 和 ref 的差异
| 特性 | ref | reactive |
|---|---|---|
| 适用对象 | 任意类型 | 对象 |
| 赋值 | 整体替换需改 .value | 不能整体替换,会丢响应式 |
| 解构 | 解构后仍保留响应式 | 解构后丢失响应式 |
// reactive 不能整体替换
let state = reactive({ count: 0 })
state = { count: 1 } // ❌ 响应式丢失
// ✅ 应逐个属性修改
state.count = 1
// 或改用 ref 包对象
const state = ref({ count: 0 })
state.value = { count: 1 } // ✅
7.3 解构用 toRefs
const state = reactive({ name: 'vue', count: 0 })
// ❌ 解构后丢失响应式
const { name } = state
// ✅ 用 toRefs 保持响应式
const { name, count } = toRefs(state)
八、完整示例:一个带类型的小组件
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
// 1. Props 类型
interface Props {
title: string
maxCount?: number
}
const props = withDefaults(defineProps<Props>(), {
maxCount: 10
})
// 2. Emit 类型
const emit = defineEmits<{
submit: [result: { count: number }]
}>()
// 3. ref
const count = ref(0)
// 4. reactive
const form = reactive({
name: '',
valid: false
})
// 5. computed
const canSubmit = computed(() => count.value <= props.maxCount)
function handleSubmit() {
if (!canSubmit.value) return
emit('submit', { count: count.value })
}
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>当前: {{ count }} / {{ maxCount }}</p>
<button @click="count++" :disabled="!canSubmit">增加</button>
<button @click="handleSubmit">提交</button>
</div>
</template>
父组件使用示例:
<script setup lang="ts">
import MyComponent from './MyComponent.vue'
function onSubmit(result: { count: number }) {
console.log('提交数量:', result.count)
}
</script>
<template>
<MyComponent title="计数器" :max-count="5" @submit="onSubmit" />
</template>
九、选型与踩坑
| 场景 | 推荐 | 说明 |
|---|---|---|
| 简单 props | defineProps<{...}>() | 直接用泛型,简洁 |
| 有默认值 | withDefaults(defineProps<Props>(), {...}) | 必须配合 withDefaults |
| 基础类型/单值 | ref | 需要 .value |
| 表单等对象 | reactive | 不要整体替换 |
| 需要整体替换对象 | ref 包对象 | 用 ref<Obj>({...}) |
| 解构 reactive | toRefs | 否则会丢响应式 |
常见坑
- defineProps 解构丢响应式:不要直接解构,需要时用
computed或toRef。 - reactive 整体替换:只能改属性,不能
state = newState。 - ref 在 template 里不用 .value:只在 script 里需要。
- emit 参数和类型不一致:用对象形式定义好参数类型,避免传错。
十、小结
| 概念 | 类型写法 | 注意点 |
|---|---|---|
| props | defineProps<Props>() + withDefaults | 有默认值必须用 withDefaults |
| emit | defineEmits<{ event: [args] }>() | 用元组定义参数 |
| ref | ref<T>(initial) 或 ref(initial) | 用 .value 访问 |
| reactive | reactive<T>(obj) | 不整体替换、解构用 toRefs |
记住三点:
- props 和 emit 用
defineXxx+ 泛型/接口,把参数类型写清楚。 - ref 适合任意类型,reactive 只适合对象,且不能整体替换。
- 有默认值用
withDefaults,解构 reactive 用toRefs。
把类型写对,IDE 提示会更好用,重构也更放心。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~