第74课:TypeScript 与 React / Vue 集成——React 类型、defineComponent、PropType、emit 类型
TypeScript 与前端框架的深度结合是现代 Web 开发的标配。React 和 Vue 3 都提供了完备的类型系统支持,使组件 Props、事件、状态和模板表达式都能够享受静态类型检查的保障。然而,两个框架在类型集成上采用了不同的哲学:React 以类型注解为核心——直接通过 TypeScript 的类型语法标注函数参数、泛型 Hooks 和事件对象;Vue 3 则通过编译器宏(defineProps、defineEmits)与 TypeScript 的配合,将类型信息嵌入到单文件组件的 <script setup> 中。本节课将并行讲解两种框架的 TypeScript 集成方式,覆盖组件类型、Props 验证、事件声明、Hooks / Composition API 的类型推导,以及构建可复用泛型组件的设计模式。
1. React 与 TypeScript:函数组件与 Hooks 的类型体系
1.1 函数组件的类型声明
在 React 中,函数组件本质上是接收 props 并返回 ReactNode 的函数。使用 TypeScript 时,可以直接对 props 参数标注接口类型。
1 | interface UserCardProps { |
React.FC 类型:React 提供了 FC(FunctionComponent)泛型类型,它自动包含 children 类型。是否使用 FC 存在社区争议——它可能让你不小心接受未预期的 children。现代 React + TypeScript 项目倾向于直接标注 props 参数,显式控制 children。
1 | import { ReactNode } from 'react'; |
若使用 PropsWithChildren 工具类型,可避免手动声明 children:
1 | import { PropsWithChildren } from 'react'; |
最佳实践:在需要明确接收 children 时使用 PropsWithChildren;在不需要 children 时直接标注 props 对象,避免无意中接受。
1.2 Hooks 的类型推断与注解
React Hooks 拥有完善的类型推断,大多数情况下无需手动提供类型参数。但在以下几种场景中,显式类型注解是必要的。
**useState**:初始值决定了类型推断。如果初始值无法推断出完整类型(如空数组、null),需显式指定泛型参数。
1 | const [count, setCount] = useState<number>(0); // 推断为 number |
**useRef**:useRef 的类型分两种场景——存储可变值 vs 绑定 DOM 元素。类型参数不同,行为也不同。
1 | // 存储可变值(不绑定 DOM) |
**useReducer**:通常为 reducer 函数的参数和返回值定义类型,TypeScript 能推断出 state 和 dispatch 的类型。
1 | type State = { count: number }; |
useEffect:通常无需类型注解,但需注意返回值的类型必须是清理函数或无返回值。TypeScript 会检查这一点。
1.3 事件处理类型
React 的合成事件(SyntheticEvent)有对应的泛型类型,用于描述不同 DOM 元素触发的事件。
1 | function handleClick(e: React.MouseEvent<HTMLButtonElement>) { |
常用事件类型映射:
| 事件 | 类型 |
|---|---|
| 点击事件 | React.MouseEvent<T> |
| 输入变更 | React.ChangeEvent<T> |
| 表单提交 | React.FormEvent<T> |
| 键盘事件 | React.KeyboardEvent<T> |
| 焦点事件 | React.FocusEvent<T> |
| 拖拽事件 | React.DragEvent<T> |
获取元素类型:如果需要获取某个 JSX 元素的类型,可以使用 React.ComponentProps<'element'> 提取。
1 | type ButtonProps = React.ComponentProps<'button'>; |
1.4 泛型组件
当组件需要根据传入数据动态决定渲染类型时,可以定义泛型组件。
1 | interface ListProps<T> { |
在 .tsx 文件中,箭头函数形式的泛型组件需使用 <T,> 语法(尾随逗号)避免与 JSX 标签冲突。
2. Vue 3 与 TypeScript:组合式 API 的类型集成
Vue 3 的 <script setup> 结合 TypeScript 提供了独特的类型声明方式——通过编译器宏 defineProps 和 defineEmits,将类型信息直接内嵌到组件逻辑中。这种方案无需额外的类型导入,语法简洁且类型安全。
2.1 defineProps 的类型声明
使用 defineProps 结合 TypeScript 纯类型声明来描述组件 Props。Vue 编译器会分析类型参数并生成运行时的 Props 验证。
1 | <script setup lang="ts"> |
带默认值的 Props:使用 withDefaults 宏。
1 | <script setup lang="ts"> |
运行时验证风格(选项式语法):也可以使用 PropType 显式标注类型,适用于需要自定义验证器的场景。
1 | import { PropType } from 'vue'; |
2.2 defineEmits 的类型声明
使用 defineEmits 结合类型参数声明组件对外抛出的事件及其负载类型。这是 Vue 3.3+ 的推荐语法。
1 | <script setup lang="ts"> |
简单场景:如果事件较少,可以直接使用函数签名简写。
1 | <script setup lang="ts"> |
2.3 ref 与 reactive 的类型推断
Vue 3 的 ref 和 reactive 具有强大的类型推断能力。
1 | import { ref, reactive } from 'vue'; |
ref 与 DOM 元素:使用 ref 绑定模板中的 DOM 元素时,需要显式指定元素类型。
1 | <script setup lang="ts"> |
2.4 computed 与 watch 的类型
computed 通常能自动推断返回值类型,但在复杂逻辑中可以显式指定。
1 | import { computed, watch, ref } from 'vue'; |
2.5 组件实例类型
当需要引用子组件的实例(通过 ref 获取子组件暴露的方法)时,可以使用 InstanceType<typeof Component> 获取组件实例类型。
1 | <script setup lang="ts"> |
子组件需要使用 defineExpose 暴露方法或属性。
2.6 defineComponent(选项式 API 场景)
对于仍使用选项式 API 的项目,defineComponent 提供完整的类型推导。
1 | import { defineComponent, PropType } from 'vue'; |
在组合式 API 成为主流的今天,推荐使用 <script setup> 语法以获得更简洁的类型声明。
3. React 与 Vue 的 TypeScript 集成对比
| 特性 | React | Vue 3 (script setup) |
|---|---|---|
| Props 类型 | 直接标注函数参数类型(interface) |
defineProps<T>() 编译器宏 |
| 事件类型 | 泛型 React.MouseEvent<T> 等 |
defineEmits<Emits>() 函数签名类型 |
| 状态类型 | useState<T>() 泛型参数 |
ref<T>()、reactive() 自动推断 |
| DOM 引用类型 | useRef<HTMLDivElement>(null) |
ref<HTMLDivElement | null>(null) |
| Children 类型 | ReactNode 或 PropsWithChildren |
<slot /> 自动推导,defineSlots 可选 |
| 泛型组件 | 箭头函数 <T,>(props: Props<T>) |
当前版本不直接支持泛型组件(可通过插件) |
| 组件实例类型 | React.ComponentRef<typeof Comp> |
InstanceType<typeof Comp> |
4. 综合实战:带类型安全表单的组件示例
4.1 React 版本
1 | interface LoginFormProps { |
4.2 Vue 3 版本
1 | <script setup lang="ts"> |
课后练习
一、概念自测(选择题 / 填空题)
(单选) React 的
useStateHook 中,如果初始值是null,需要在什么情况下显式指定泛型参数?
A. 总是需要。
B. 不需要,TypeScript 自动推断。
C. 若后续可能赋值为另一种类型,需指定联合类型。
D. 无法在useState中使用泛型。(单选) 在 Vue 3
<script setup>中,为defineProps提供类型声明的正确方式是?
A.const props = defineProps({ name: String })
B.const props = defineProps<UserProps>()
C.defineProps<{ name: string }>()
D. B 和 C 均正确(填空) 在 React 中获取输入框
onChange事件的正确事件类型是React.______<HTMLInputElement>。(多选) 以下哪些是 Vue 3 中声明
emits的合法方式?
A.defineEmits<{ (e: 'submit'): void }>()
B.defineEmits<{ submit: [] }>()
C.defineEmits(['submit'])
D.defineEmits<{ submit: [data: FormData] }>()
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要实现一个通用的 Toggle 组件,它可以渲染任意内容,并暴露 on 状态和 toggle 方法。要求如下:
- React 版本:使用 TypeScript 编写一个 Render Props 模式的组件,类型安全地传递
on和toggle。 - Vue 3 版本:使用
<script setup lang="ts">编写一个作用域插槽(Scoped Slot)模式的组件,类型安全地暴露on和toggle。 - 两个版本都需包含完整的类型声明(Props、Slots/Render Props),确保外部使用时有完整的类型提示。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其分别生成 React 和 Vue 3 的 Toggle 组件代码。提示词中需明确指定组件的行为、类型声明方式以及子组件/插槽的使用示例。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请同时为 React 和 Vue 3 创建通用的 Toggle 组件,具备完整的 TypeScript 类型。
React 版本(
src/components/Toggle.tsx):
- 组件接收
children: (state: { on: boolean; toggle: () => void }) => ReactNode作为 render prop。- 内部使用
useState(false)管理on状态,toggle函数反转状态。- 使用
interface ToggleProps定义 Props 类型。导出组件。- 在文件末尾添加一个使用示例:
<Toggle>{({ on, toggle }) => <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>}</Toggle>。Vue 3 版本(
src/components/Toggle.vue):
- 使用
<script setup lang="ts">。- 内部使用
ref(false)管理on状态,toggle函数反转状态。- 不接收任何 props,但通过作用域插槽暴露
on: boolean和toggle: () => void。使用defineSlots<{ default: (props: { on: boolean; toggle: () => void }) => any }>()声明插槽类型。- 模板中使用
<slot :on="on" :toggle="toggle" />。- 在组件下方添加使用示例(在父组件中)。
所有代码使用严格 TypeScript,注释说明类型声明方式。完成后列出所有文件的内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请对比 React 和 Vue 3 在 TypeScript 集成上的设计差异。分别说明两者如何声明组件的 Props 类型,以及为何 Vue 3 的
defineProps需要通过编译器宏而非普通类型注解来实现?
参考答案:
React 采用纯类型注解方式:Props 直接作为函数组件的参数类型(function Comp(props: MyProps)),类型信息在编译时完全可用,运行时被移除。这是 React 一贯的“JavaScript 即类型”哲学的延伸——类型只是附加在代码上的元数据。
Vue 3 的 defineProps 则是一个编译器宏。这个设计的原因是:Vue 的单文件组件在编译时由 @vue/compiler-sfc 处理。Props 的类型信息不仅用于静态检查,还需要在运行时生成组件元数据(如 Props 验证、文档生成)。defineProps 允许编译器从类型参数中提取信息并生成运行时代码。如果仅靠普通类型注解,TypeScript 在编译后会擦除所有类型信息,Vue 运行时将无法获知组件的 Props 结构。defineProps 宏在编译阶段被转换为包含运行时验证的 Props 定义对象,同时保留类型检查。
两者都实现了类型安全的组件开发,但 React 依赖开发者在代码中保持类型注解的一致性,而 Vue 通过编译器深度融合类型与运行时。React 的方案更轻量、更灵活(如泛型组件),Vue 的方案则在模板中提供了更完整的推导(如自动推断插槽 props 类型)。
课后练习答案
一、概念自测答案
C
- 解析:如果初始值为
null,但后续可能赋值为其他类型(如User | null),必须显式指定useState<User | null>(null),否则 TypeScript 推断类型为null。
- 解析:如果初始值为
D
- 解析:
defineProps<UserProps>()和defineProps<{ name: string }>()均可。前者使用独立接口,后者内联类型。A 是运行时声明方式(非纯类型)。
- 解析:
ChangeEvent- 解析:
React.ChangeEvent<HTMLInputElement>是输入框 onChange 事件的标准类型。
- 解析:
A、B、C、D
- 解析:A 是函数签名写法(3.2-),B 是简化对象写法(3.3+),C 是运行时数组声明(非类型安全),D 是带负载的简化写法(3.3+)。均为合法。
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请分别用 React + TypeScript 和 Vue 3 + TypeScript 实现一个通用的Toggle组件。要求:
- React 版本使用 render props 模式,Props 接口包含
children: (state: { on: boolean; toggle: () => void }) => ReactNode。内部使用useState管理状态。提供完整使用示例。- Vue 3 版本使用
<script setup lang="ts">和作用域插槽。使用defineSlots声明插槽类型。内部使用ref管理状态。提供父组件使用示例。- 所有代码具有完整类型注解,注释清晰。输出两个文件的完整内容。”