WinddSnow

TypeScript-Integration-with-React-and-Vue

字数统计: 4.3k阅读时长: 19 min
2026/07/31

第74课:TypeScript 与 React / Vue 集成——React 类型、defineComponentPropTypeemit 类型

TypeScript 与前端框架的深度结合是现代 Web 开发的标配。React 和 Vue 3 都提供了完备的类型系统支持,使组件 Props、事件、状态和模板表达式都能够享受静态类型检查的保障。然而,两个框架在类型集成上采用了不同的哲学:React 以类型注解为核心——直接通过 TypeScript 的类型语法标注函数参数、泛型 Hooks 和事件对象;Vue 3 则通过编译器宏definePropsdefineEmits)与 TypeScript 的配合,将类型信息嵌入到单文件组件的 <script setup> 中。本节课将并行讲解两种框架的 TypeScript 集成方式,覆盖组件类型、Props 验证、事件声明、Hooks / Composition API 的类型推导,以及构建可复用泛型组件的设计模式。


1. React 与 TypeScript:函数组件与 Hooks 的类型体系

1.1 函数组件的类型声明

在 React 中,函数组件本质上是接收 props 并返回 ReactNode 的函数。使用 TypeScript 时,可以直接对 props 参数标注接口类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface UserCardProps {
name: string;
age: number;
email?: string;
}

function UserCard({ name, age, email }: UserCardProps) {
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
{email && <p>Email: {email}</p>}
</div>
);
}

React.FC 类型:React 提供了 FCFunctionComponent)泛型类型,它自动包含 children 类型。是否使用 FC 存在社区争议——它可能让你不小心接受未预期的 children。现代 React + TypeScript 项目倾向于直接标注 props 参数,显式控制 children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ReactNode } from 'react';

interface CardProps {
title: string;
children?: ReactNode; // 显式声明 children
}

function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}

若使用 PropsWithChildren 工具类型,可避免手动声明 children

1
2
3
4
5
6
7
8
9
import { PropsWithChildren } from 'react';

interface CardProps {
title: string;
}

function Card({ title, children }: PropsWithChildren<CardProps>) {
return <div>{children}</div>;
}

最佳实践:在需要明确接收 children 时使用 PropsWithChildren;在不需要 children 时直接标注 props 对象,避免无意中接受。

1.2 Hooks 的类型推断与注解

React Hooks 拥有完善的类型推断,大多数情况下无需手动提供类型参数。但在以下几种场景中,显式类型注解是必要的。

**useState**:初始值决定了类型推断。如果初始值无法推断出完整类型(如空数组、null),需显式指定泛型参数。

1
2
3
const [count, setCount] = useState<number>(0);          // 推断为 number
const [user, setUser] = useState<User | null>(null); // 联合类型,初始 null
const [items, setItems] = useState<string[]>([]); // 空数组必须注解

**useRef**:useRef 的类型分两种场景——存储可变值 vs 绑定 DOM 元素。类型参数不同,行为也不同。

1
2
3
4
5
6
// 存储可变值(不绑定 DOM)
const intervalRef = useRef<number | null>(null);

// 绑定 DOM 元素(只读)
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current 的类型为 HTMLInputElement | null

**useReducer**:通常为 reducer 函数的参数和返回值定义类型,TypeScript 能推断出 statedispatch 的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset'; payload: number };

function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: action.payload };
}
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
// state: State, dispatch: Dispatch<Action>

useEffect:通常无需类型注解,但需注意返回值的类型必须是清理函数或无返回值。TypeScript 会检查这一点。

1.3 事件处理类型

React 的合成事件(SyntheticEvent)有对应的泛型类型,用于描述不同 DOM 元素触发的事件。

1
2
3
4
5
6
7
8
9
10
11
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.currentTarget); // HTMLButtonElement
}

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value); // string
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}

常用事件类型映射

事件 类型
点击事件 React.MouseEvent<T>
输入变更 React.ChangeEvent<T>
表单提交 React.FormEvent<T>
键盘事件 React.KeyboardEvent<T>
焦点事件 React.FocusEvent<T>
拖拽事件 React.DragEvent<T>

获取元素类型:如果需要获取某个 JSX 元素的类型,可以使用 React.ComponentProps<'element'> 提取。

1
2
type ButtonProps = React.ComponentProps<'button'>;
type InputProps = React.ComponentProps<'input'>;

1.4 泛型组件

当组件需要根据传入数据动态决定渲染类型时,可以定义泛型组件。

1
2
3
4
5
6
7
8
9
10
11
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}

// 使用时自动推断 T
<List items={[1, 2, 3]} renderItem={(n) => <span>{n * 2}</span>} />

.tsx 文件中,箭头函数形式的泛型组件需使用 <T,> 语法(尾随逗号)避免与 JSX 标签冲突。


2. Vue 3 与 TypeScript:组合式 API 的类型集成

Vue 3 的 <script setup> 结合 TypeScript 提供了独特的类型声明方式——通过编译器宏 definePropsdefineEmits,将类型信息直接内嵌到组件逻辑中。这种方案无需额外的类型导入,语法简洁且类型安全。

2.1 defineProps 的类型声明

使用 defineProps 结合 TypeScript 纯类型声明来描述组件 Props。Vue 编译器会分析类型参数并生成运行时的 Props 验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
interface UserCardProps {
name: string;
age: number;
email?: string;
}

const props = defineProps<UserCardProps>();
// props.name 的类型为 string,props.age 的类型为 number
</script>

<template>
<div>
<h3>{{ props.name }}</h3>
<p>Age: {{ props.age }}</p>
<p v-if="props.email">Email: {{ props.email }}</p>
</div>
</template>

带默认值的 Props:使用 withDefaults 宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
interface ButtonProps {
type?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
}

const props = withDefaults(defineProps<ButtonProps>(), {
type: 'primary',
size: 'medium',
disabled: false,
});
</script>

运行时验证风格(选项式语法):也可以使用 PropType 显式标注类型,适用于需要自定义验证器的场景。

1
2
3
4
5
6
7
8
9
import { PropType } from 'vue';

defineProps({
status: {
type: String as PropType<'active' | 'inactive' | 'pending'>,
required: true,
validator: (value: string) => ['active', 'inactive', 'pending'].includes(value),
},
});

2.2 defineEmits 的类型声明

使用 defineEmits 结合类型参数声明组件对外抛出的事件及其负载类型。这是 Vue 3.3+ 的推荐语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup lang="ts">
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'submit', data: { name: string; age: number }): void;
(e: 'cancel'): void;
}

const emit = defineEmits<Emits>();

function handleSubmit() {
emit('submit', { name: 'Alice', age: 25 }); // ✅ 类型安全
// emit('submit', { name: 'Alice' }); // ❌ 编译错误:缺少 age
}
</script>

简单场景:如果事件较少,可以直接使用函数签名简写。

1
2
3
4
5
6
<script setup lang="ts">
const emit = defineEmits<{
'update:modelValue': [value: string];
'submit': [];
}>();
</script>

2.3 refreactive 的类型推断

Vue 3 的 refreactive 具有强大的类型推断能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref, reactive } from 'vue';

// ref 自动推断类型
const count = ref(0); // Ref<number>
const message = ref<string>(''); // 显式指定 string(初始空字符串时需注解)
const user = ref<User | null>(null); // 联合类型

// reactive 推断对象类型
const state = reactive({
name: 'Alice',
age: 25,
hobbies: ['reading'] as string[], // 使用 as 断言数组元素类型
});

ref 与 DOM 元素:使用 ref 绑定模板中的 DOM 元素时,需要显式指定元素类型。

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import { ref } from 'vue';

const inputRef = ref<HTMLInputElement | null>(null);
</script>

<template>
<input ref="inputRef" />
</template>

2.4 computedwatch 的类型

computed 通常能自动推断返回值类型,但在复杂逻辑中可以显式指定。

1
2
3
4
5
6
7
8
9
10
import { computed, watch, ref } from 'vue';

const doubled = computed<number>(() => count.value * 2);

watch(
() => user.value?.name,
(newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`);
}
);

2.5 组件实例类型

当需要引用子组件的实例(通过 ref 获取子组件暴露的方法)时,可以使用 InstanceType<typeof Component> 获取组件实例类型。

1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
import { ref } from 'vue';
import MyModal from './MyModal.vue';

const modalRef = ref<InstanceType<typeof MyModal> | null>(null);

function openModal() {
modalRef.value?.open();
}
</script>

子组件需要使用 defineExpose 暴露方法或属性。

2.6 defineComponent(选项式 API 场景)

对于仍使用选项式 API 的项目,defineComponent 提供完整的类型推导。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { defineComponent, PropType } from 'vue';

export default defineComponent({
props: {
name: { type: String, required: true },
age: { type: Number, default: 0 },
role: { type: String as PropType<'admin' | 'user'>, default: 'user' },
},
emits: {
submit: (payload: { name: string }) => true,
},
setup(props, { emit }) {
// props 和 emit 自动获得类型
emit('submit', { name: props.name });
},
});

在组合式 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 类型 ReactNodePropsWithChildren <slot /> 自动推导,defineSlots 可选
泛型组件 箭头函数 <T,>(props: Props<T>) 当前版本不直接支持泛型组件(可通过插件)
组件实例类型 React.ComponentRef<typeof Comp> InstanceType<typeof Comp>

4. 综合实战:带类型安全表单的组件示例

4.1 React 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface LoginFormProps {
onSubmit: (data: { email: string; password: string }) => void;
loading?: boolean;
}

function LoginForm({ onSubmit, loading = false }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};

return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} required />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} required />
<button type="submit" disabled={loading}>{loading ? '登录中...' : '登录'}</button>
</form>
);
}

4.2 Vue 3 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup lang="ts">
import { ref } from 'vue';

interface LoginFormData {
email: string;
password: string;
}

const props = defineProps<{
loading?: boolean;
}>();

const emit = defineEmits<{
submit: [data: LoginFormData];
}>();

const email = ref('');
const password = ref('');

function handleSubmit() {
emit('submit', { email: email.value, password: password.value });
}
</script>

<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" type="email" required />
<input v-model="password" type="password" required />
<button type="submit" :disabled="props.loading">
{{ props.loading ? '登录中...' : '登录' }}
</button>
</form>
</template>

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) React 的 useState Hook 中,如果初始值是 null,需要在什么情况下显式指定泛型参数?
    A. 总是需要。
    B. 不需要,TypeScript 自动推断。
    C. 若后续可能赋值为另一种类型,需指定联合类型。
    D. 无法在 useState 中使用泛型。

  2. (单选) 在 Vue 3 <script setup> 中,为 defineProps 提供类型声明的正确方式是?
    A. const props = defineProps({ name: String })
    B. const props = defineProps<UserProps>()
    C. defineProps<{ name: string }>()
    D. B 和 C 均正确

  3. (填空) 在 React 中获取输入框 onChange 事件的正确事件类型是 React.______<HTMLInputElement>

  4. (多选) 以下哪些是 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 模式的组件,类型安全地传递 ontoggle
  • Vue 3 版本:使用 <script setup lang="ts"> 编写一个作用域插槽(Scoped Slot)模式的组件,类型安全地暴露 ontoggle
  • 两个版本都需包含完整的类型声明(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: booleantoggle: () => 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 类型)。


课后练习答案

一、概念自测答案

  1. C

    • 解析:如果初始值为 null,但后续可能赋值为其他类型(如 User | null),必须显式指定 useState<User | null>(null),否则 TypeScript 推断类型为 null
  2. D

    • 解析:defineProps<UserProps>()defineProps<{ name: string }>() 均可。前者使用独立接口,后者内联类型。A 是运行时声明方式(非纯类型)。
  3. ChangeEvent

    • 解析:React.ChangeEvent<HTMLInputElement> 是输入框 onChange 事件的标准类型。
  4. 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 管理状态。提供父组件使用示例。
  • 所有代码具有完整类型注解,注释清晰。输出两个文件的完整内容。”
CATALOG
  1. 1. 第74课:TypeScript 与 React / Vue 集成——React 类型、defineComponent、PropType、emit 类型
    1. 1.1. 1. React 与 TypeScript:函数组件与 Hooks 的类型体系
      1. 1.1.1. 1.1 函数组件的类型声明
      2. 1.1.2. 1.2 Hooks 的类型推断与注解
      3. 1.1.3. 1.3 事件处理类型
      4. 1.1.4. 1.4 泛型组件
    2. 1.2. 2. Vue 3 与 TypeScript:组合式 API 的类型集成
      1. 1.2.1. 2.1 defineProps 的类型声明
      2. 1.2.2. 2.2 defineEmits 的类型声明
      3. 1.2.3. 2.3 ref 与 reactive 的类型推断
      4. 1.2.4. 2.4 computed 与 watch 的类型
      5. 1.2.5. 2.5 组件实例类型
      6. 1.2.6. 2.6 defineComponent(选项式 API 场景)
    3. 1.3. 3. React 与 Vue 的 TypeScript 集成对比
    4. 1.4. 4. 综合实战:带类型安全表单的组件示例
      1. 1.4.1. 4.1 React 版本
      2. 1.4.2. 4.2 Vue 3 版本
    5. 1.5. 课后练习
      1. 1.5.1. 一、概念自测(选择题 / 填空题)
      2. 1.5.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.5.3. 三、Agent 模式下的提示词示例
      4. 1.5.4. 四、面试真题与参考答案
    6. 1.6. 课后练习答案
      1. 1.6.1. 一、概念自测答案
      2. 1.6.2. 二、AI 编程任务参考答案(提示词示例)