TypeScript学习-第12章:与主流框架结合
上一章咱们把tsconfig.json玩得明明白白,TS编译规则总算尽在掌握。可一进入React/Vue项目,又瞬间破防:组件Props传参随心所欲,类型错误藏到运行时才暴露;Hooks状态乱改,不知道该标注什么类型;框架API和TS类型打架,最后只能用any"摆烂"......其实TS和主流框架本是"黄金搭档",前者给后者的灵活性套上"类型安全锁",后者让TS的类型能力落地到真实业务。今天咱们就手把手搞定TS与React、Vue3的结合,从此在框架项目中告别any,写出又稳又优雅的代码。
一、TypeScript + React:给组件加"类型身份证"
React的核心是组件和Hooks,TS与React结合的关键,就是给Props、状态、事件、Hooks都加上精准类型,让组件的"输入输出"和内部逻辑都有迹可循。咱们从核心场景逐个突破。
1. 组件Props类型约束:拒绝"乱传参"
Props是组件的"输入参数",用TS给Props定义类型,就像给组件贴了"身份证",传错参数直接编译报错,再也不用靠console.log调试传参问题。核心是用接口(interface)定义Props结构,支持可选属性、默认值等场景。
tsx
// 1. 基础Props类型:必选+可选属性
import React from 'react';
// 定义Props接口
interface UserCardProps {
name: string; // 必选属性
age: number; // 必选属性
avatar?: string; // 可选属性(用?标记)
tags: string[]; // 数组类型
}
// 函数组件接收Props,标注类型
const UserCard: React.FC<UserCardProps> = (props) => {
const { name, age, avatar = '/default-avatar.png', tags } = props;
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}({age}岁)</h3>
<div className="tags">
{tags.map(tag => <span key={tag}>{tag}</span>)}
</div>
</div>
);
};
// 使用组件:传参错误会直接报错
<UserCard name="张三" age={25} tags={['前端', 'TS']} /> // 合法
<UserCard name="李四" age="25" tags={['前端']} /> // 报错:age应为number类型
小提示:React.FC(FunctionComponent)是TS提供的函数组件类型,会自动包含children属性;若组件不需要children,也可直接标注函数参数类型(如 (props: UserCardProps) => JSX.Element),更灵活。
2. 事件处理类型:精准标注事件对象
React事件处理是高频场景,直接用any标注事件对象会丢失类型提示,TS提供了对应事件类型(如React.MouseEvent、React.ChangeEvent),精准标注事件源和事件属性。
tsx
const InputDemo: React.FC = () => {
// 输入框change事件:标注为ChangeEvent<HTMLInputElement>
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('输入内容:', e.target.value); // 自动提示target、value属性
};
// 按钮点击事件:标注为MouseEvent<HTMLButtonElement>
const handleBtnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); // 事件对象方法自动提示
console.log('按钮被点击');
};
return (
<div>
<input type="text" onChange={handleInputChange} />
<button onClick={handleBtnClick}>点击我</button>
</div>
);
};
核心规律:React事件类型格式为XXXEvent<事件源元素类型>,常见事件类型:ChangeEvent(输入变化)、MouseEvent(鼠标操作)、FormEvent(表单提交)。
3. Hooks类型标注:让状态和副作用有约束
React Hooks与TS结合时,大部分场景TS能自动推导类型,复杂场景需手动标注,核心覆盖useState、useRef、useEffect三大常用Hook。
tsx
const HooksDemo: React.FC = () => {
// 1. useState:简单类型自动推导,复杂类型可手动标注
const [count, setCount] = React.useState(0); // 自动推导count为number
const [user, setUser] = React.useState<{ name: string; age: number } | null>(null); // 手动标注联合类型
// 2. useRef:分两种场景标注(DOM元素/普通值)
// DOM元素场景:标注为HTMLDivElement,初始值为null
const boxRef = React.useRef<HTMLDivElement>(null);
// 普通值场景:标注为number,初始值为0
const timerRef = React.useRef<number>(0);
// 3. useEffect:依赖项类型由外部状态推导,无需额外标注
React.useEffect(() => {
timerRef.current = window.setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 操作DOM:需先判断非空(strict模式下必做)
if (boxRef.current) {
boxRef.current.style.color = 'red';
}
return () => clearInterval(timerRef.current);
}, []);
return <div ref={boxRef}>计数:{count}</div>;
};
二、TypeScript + Vue3:组合式API的类型魔法
Vue3的组合式API(Composition API)天生对TS友好,<script setup lang="ts"> 语法可直接开启TS支持,核心是给ref、reactive、Props、Emits等标注类型,让组合逻辑更安全。
1. 组合式API类型标注:ref/reactive/computed
ref用于基础类型状态,reactive用于复杂对象状态,computed依赖状态推导类型,TS能自动适配大部分场景,复杂场景需手动标注泛型。
vue
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
// 1. ref:基础类型自动推导,复杂类型标注泛型
const count = ref(0); // 自动推导为Ref<number>
const username = ref<string | null>(null); // 手动标注联合类型
// 2. reactive:传入对象自动推导类型,无需额外标注
interface User {
name: string;
age: number;
hobbies: string[];
}
const user = reactive<User>({
name: '张三',
age: 25,
hobbies: ['前端', 'TS']
});
// 3. computed:根据依赖状态自动推导返回类型
const fullName = computed(() => {
return `姓名:${user.name}`; // 自动推导为ComputedRef<string>
});
// 状态操作:类型错误直接报错
count.value = '123'; // 报错:应为number类型
user.hobbies.push(123); // 报错:hobbies元素应为string类型
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>用户名:{{ username }}</p>
<p>完整姓名:{{ fullName }}</p>
</div>
</template>
核心提醒:ref包裹的状态需通过 .value 访问,TS会自动提示该属性;reactive包裹的对象直接访问属性,类型推导更直观。
2. 组件Props与Emits类型约束
Vue3中用defineProps定义Props,defineEmits定义事件,结合TS泛型可精准标注Props类型和事件参数类型,替代传统的props/emits选项。
vue
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
// 1. 定义Props类型:用泛型标注接口
interface CardProps {
title: string;
desc?: string; // 可选属性
size: 'small' | 'middle' | 'large'; // 联合类型约束
}
const props = defineProps<CardProps>();
// 2. 定义Emits类型:标注事件名和参数类型
const emits = defineEmits<{
(e: 'click', id: number): void; // 事件名click,参数为number
(e: 'change', value: string): void; // 事件名change,参数为string
}>();
// 触发事件:参数类型错误报错
const handleClick = () => {
emits('click', 123); // 合法
emits('click', '123'); // 报错:参数应为number类型
};
</script>
<template>
<div class={`card card-${size}`}>
<h3>{{ title }}</h3>
<p v-if="desc">{{ desc }}</p>
<button @click="handleClick">触发事件</button>
</div>
</template>
3. Pinia类型封装:状态管理的类型安全
Pinia是Vue3官方推荐的状态管理库,天生对TS友好,无需额外类型标注即可获得完整类型提示,核心是定义Store时传入状态类型,自动推导Actions和Getters类型。
ts
// store/userStore.ts
import { defineStore } from 'pinia';
// 定义状态类型
interface UserState {
name: string;
age: number;
isLogin: boolean;
}
// 定义Store:自动推导状态、Actions类型
export const useUserStore = defineStore('user', {
// 状态:标注为UserState类型
state: (): UserState => ({
name: '张三',
age: 25,
isLogin: false
}),
// Getters:自动推导返回类型
getters: {
fullName: (state) => `用户:${state.name}`, // 推导为string
isAdult: (state) => state.age >= 18 // 推导为boolean
},
// Actions:参数和返回值自动推导类型
actions: {
login(name: string, age: number) {
this.name = name;
this.age = age;
this.isLogin = true;
},
logout() {
this.isLogin = false;
}
}
});
// 组件中使用:完整类型提示
<script setup lang="ts">
import { useUserStore } from '@/store/userStore';
const userStore = useUserStore();
userStore.login('李四', 28); // 自动提示参数类型
console.log(userStore.fullName); // 自动提示Getters属性
</script>
三、实战:用TS写一个React TodoItem组件
咱们选React框架,写一个完整的TodoItem组件,整合Props、状态、事件处理的类型标注,实现"添加-删除"功能,展示TS在真实组件中的落地。
tsx
// components/TodoItem.tsx
import React, { useState } from 'react';
// 1. 定义Todo类型接口
export interface Todo {
id: number;
content: string;
completed: boolean;
}
// 2. 定义组件Props类型
interface TodoItemProps {
todo: Todo;
onDelete: (id: number) => void; // 删除事件:参数为todo的id
onToggle: (id: number) => void; // 切换完成状态事件
}
// 3. 函数组件:标注Props类型,无多余children用普通函数标注
const TodoItem = (props: TodoItemProps): React.ReactElement => {
const { todo, onDelete, onToggle } = props;
// 本地状态:输入框编辑内容
const [editContent, setEditContent] = useState(todo.content);
// 输入框变化事件:标注ChangeEvent
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditContent(e.target.value);
};
// 切换完成状态事件
const handleToggle = () => {
onToggle(todo.id);
};
// 删除事件
const handleDelete = () => {
onDelete(todo.id);
};
return (
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={handleToggle}
/>
<input
type="text"
value={editContent}
onChange={handleInputChange}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
/>
<button onClick={handleDelete} style={{ marginLeft: '10px' }}>
删除
</button>
</div>
);
};
export default TodoItem;
// 页面中使用组件
// pages/TodoList.tsx
import React, { useState } from 'react';
import TodoItem, { Todo } from '../components/TodoItem';
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, content: '学习TS与React结合', completed: false },
{ id: 2, content: '写一个Todo组件', completed: true }
]);
// 删除Todo
const handleDelete = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// 切换Todo完成状态
const handleToggle = (id: number) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<div className="todo-list">
<h2>TS + React TodoList</h2>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
onToggle={handleToggle}
/>
))}
</div>
);
};
export default TodoList;
实战亮点:组件拆分清晰,Todo类型全局复用,Props和事件参数都有精准类型约束,传参错误、状态修改错误都会在编译时暴露,大幅降低运行时风险。
四、避坑指南与深度总结
-
React避免滥用React.FC:React.FC会强制添加children属性,若组件不需要children,直接标注函数参数类型更简洁,避免冗余类型。
-
Vue3 ref与reactive选型:基础类型(string/number)用ref,复杂对象用reactive;ref需通过.value访问,TS会自动提示,无需刻意记忆。
-
事件类型不要用any:React和Vue都提供了内置事件类型,按需查找对应类型(如React.FormEvent、Vue的Event),既能获得提示又能保证安全。
-
框架类型声明优先用官方包:React/Vue/Pinia的类型声明已内置或在@types包中,无需手动编写,安装依赖后自动获得类型提示。
-
严格模式贯穿始终:框架项目的tsconfig.json务必开启strict: true,否则TS的类型校验会打折扣,很多潜在问题无法暴露。
最后总结:TS与主流框架结合的核心,不是"多写类型标注",而是"用类型精准覆盖框架的核心场景"------React的Props、Hooks、事件,Vue3的组合式API、Store,都通过类型把"模糊的约定"变成"明确的约束"。这样既能享受框架的灵活性,又能借助TS的类型安全,让大型项目的开发、维护更高效。至此,咱们的TS学习也从基础语法落地到了框架实战,从此可以自信地用TS开发各类前端项目了!