TypeScript 与 React 最佳实践
React 与 TypeScript 的结合是现代前端开发的主流选择。本章将深入介绍如何在 React 项目中正确使用 TypeScript,包括组件类型定义、Hooks 类型、事件处理、表单处理等核心内容。
项目配置
创建项目
使用 Vite 创建 React TypeScript 项目是最推荐的方式:
# 创建项目
npm create vite@latest my-app -- --template react-ts
# 或使用 yarn
yarn create vite my-app --template react-ts
tsconfig.json 配置
React 项目的推荐 TypeScript 配置:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
}
关键配置解释:
jsx: "react-jsx":使用 React 17+ 的新 JSX 转换,无需手动导入 ReactisolatedModules: true:确保每个文件可以独立编译,这对于使用 Babel 或 esbuild 等工具很重要noEmit: true:不生成输出文件,由 Vite 等构建工具处理
函数组件
基本组件定义
推荐使用普通函数定义组件,而不是 React.FC:
// 推荐:普通函数定义
type ButtonProps = {
label: string;
onClick: () => void;
};
function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
// 或者使用箭头函数
const Button = ({ label, onClick }: ButtonProps) => {
return <button onClick={onClick}>{label}</button>;
};
为什么不推荐 React.FC:
// 不推荐:使用 React.FC
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
// React.FC 的问题:
// 1. 以前会隐式添加 children 类型,现在不再这样
// 2. 不支持泛型
// 3. 组件扩展更复杂
// 4. 代码更冗长,没有实际收益
组件返回类型
通常不需要显式声明返回类型,TypeScript 会自动推断:
// 推荐:让 TypeScript 自动推断返回类型
function UserCard({ name, age }: { name: string; age: number }) {
return (
<div>
<h2>{name}</h2>
<p>年龄: {age}</p>
</div>
);
}
// 必要时可以显式声明
function ConditionalRender({ show }: { show: boolean }): React.ReactNode {
if (!show) {
return null; // 明确返回 null
}
return <div>显示内容</div>;
}
扩展 HTML 元素
创建包装原生 HTML 元素的组件时,正确继承其属性:
// 继承 button 元素的所有属性
type BaseButtonProps = React.ComponentPropsWithoutRef<"button">;
type ButtonProps = BaseButtonProps & {
variant?: "primary" | "secondary";
};
function Button({ variant = "primary", ...props }: ButtonProps) {
const className = variant === "primary" ? "btn-primary" : "btn-secondary";
return <button className={className} {...props} />;
}
// 使用时可以获得完整的 button 属性提示
<Button type="submit" disabled onClick={() => {}}>
提交
</Button>;
ComponentPropsWithoutRef vs ComponentPropsWithRef:
// ComponentPropsWithoutRef - 不包含 ref
type Props1 = React.ComponentPropsWithoutRef<"input">;
// ComponentPropsWithRef - 包含 ref
type Props2 = React.ComponentPropsWithRef<"input">;
// 如果组件需要转发 ref,使用 ComponentPropsWithRef
forwardRef 使用
使用 forwardRef 转发 ref:
import { forwardRef } from "react";
type InputProps = React.ComponentPropsWithoutRef<"input"> & {
label: string;
};
// forwardRef 的两个泛型参数:ref 类型、props 类型
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
);
// 使用
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input ref={inputRef} label="用户名" />;
}
Props 类型定义
基本类型
type UserCardProps = {
name: string; // 必需字符串
age?: number; // 可选数字
isActive: boolean; // 布尔值
tags: string[]; // 字符串数组
id: string | number; // 联合类型
role: "admin" | "user" | "guest"; // 字面量类型
user: { name: string; email: string }; // 对象类型
};
children 类型
React 18+ 推荐使用 React.ReactNode:
type CardProps = {
title: string;
children: React.ReactNode; // 接受任何可渲染的内容
};
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="content">{children}</div>
</div>
);
}
// React.ReactNode 可以接受:
// - 字符串、数字
// - JSX 元素
// - 数组
// - null、undefined、boolean
// - Fragment
ReactNode vs ReactElement vs JSX.Element:
// ReactNode - 最宽泛,包含所有可渲染内容
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;
// ReactElement - JSX 元素的类型
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
// JSX.Element - 全局命名空间中的元素类型
// 通常等同于 ReactElement
// 推荐使用 React.ReactNode 作为 children 类型
函数类型 Props
type ButtonProps = {
onClick: () => void;
onClickWithEvent: (e: React.MouseEvent<HTMLButtonElement>) => void;
onHover?: (x: number, y: number) => void;
};
function Button({ onClick, onClickWithEvent, onHover }: ButtonProps) {
return (
<button onClick={onClickWithEvent} onMouseMove={(e) => onHover?.(e.clientX, e.clientY)}>
点击
</button>
);
}
Render Props 模式
type ListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用
<List
items={[{ id: 1, name: "张三" }, { id: 2, name: "李四" }]}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
泛型组件
创建泛型组件以支持不同数据类型:
type SelectProps<T extends string> = {
options: { value: T; label: string }[];
value: T;
onChange: (value: T) => void;
};
// 泛型组件定义
function Select<T extends string>({
options,
value,
onChange
}: SelectProps<T>) {
return (
<select value={value} onChange={(e) => onChange(e.target.value as T)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
// 箭头函数泛型组件写法
const SelectArrow = <T extends string>({
options,
value,
onChange,
}: SelectProps<T>) => {
return (
<select value={value} onChange={(e) => onChange(e.target.value as T)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
// 使用 - TypeScript 会自动推断类型
<Select
options={[
{ value: "a", label: "选项 A" },
{ value: "b", label: "选项 B" },
]}
value="a"
onChange={(value) => console.log(value)} // value 类型为 "a" | "b"
/>;
条件 Props
使用联合类型和判别属性实现条件 Props:
// 方式一:使用判别属性(推荐)
type ButtonProps = {
variant: "button";
onClick: () => void;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
type LinkProps = {
variant: "link";
href: string;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
type Props = ButtonProps | LinkProps;
function Clickable(props: Props) {
if (props.variant === "link") {
return <a {...props} />;
}
const { variant, ...rest } = props;
return <button {...rest} />;
}
// 使用
<Clickable variant="button" onClick={() => {}}>按钮</Clickable>
<Clickable variant="link" href="/about">链接</Clickable>
// 方式二:互斥属性
type TextProps = {
text?: string;
children?: never; // 不能同时使用 text 和 children
};
type ChildrenProps = {
text?: never;
children: React.ReactNode;
};
type Props = (TextProps | ChildrenProps) & {
size?: "small" | "medium" | "large";
};
function Label({ size = "medium", ...props }: Props) {
const content = "text" in props ? props.text : props.children;
return <span className={`label-${size}`}>{content}</span>;
}
// 使用
<Label text="标题" /> // 正确
<Label children="内容" /> // 正确
<Label text="标题">内容</Label> // 错误:不能同时使用
事件处理
事件类型
常用的事件类型:
// 鼠标事件
function MouseExample() {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("点击坐标:", e.clientX, e.clientY);
console.log("目标元素:", e.currentTarget);
};
const handleDoubleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault(); // 阻止默认行为
};
return (
<div onDoubleClick={handleDoubleClick}>
<button onClick={handleClick}>点击</button>
</div>
);
}
// 表单事件
function FormExample() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("表单提交");
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log("输入值:", e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button type="submit">提交</button>
</form>
);
}
// 键盘事件
function KeyboardExample() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
console.log("按下回车");
}
if (e.ctrlKey && e.key === "s") {
e.preventDefault();
console.log("保存");
}
};
return <input onKeyDown={handleKeyDown} />;
}
// 焦点事件
function FocusExample() {
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
console.log("获得焦点");
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
console.log("失去焦点");
};
return <input onFocus={handleFocus} onBlur={handleBlur} />;
}
// 拖拽事件
function DragExample() {
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData("text/plain", "拖拽数据");
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const data = e.dataTransfer.getData("text/plain");
console.log("放下数据:", data);
};
return (
<div draggable onDragStart={handleDragStart} onDrop={handleDrop}>
拖拽我
</div>
);
}
事件类型速查表
| 事件类型 | TypeScript 类型 | 常用场景 |
|---|---|---|
| 点击 | React.MouseEvent<T> | onClick, onDoubleClick |
| 输入 | React.ChangeEvent<T> | onChange |
| 表单提交 | React.FormEvent<T> | onSubmit |
| 键盘 | React.KeyboardEvent<T> | onKeyDown, onKeyUp |
| 焦点 | React.FocusEvent<T> | onFocus, onBlur |
| 拖拽 | React.DragEvent<T> | onDragStart, onDrop |
| 滚动 | React.UIEvent<T> | onScroll |
| 剪贴板 | React.ClipboardEvent<T> | onCopy, onPaste |
事件处理器类型
type EventHandler<T extends Element, E extends Event> = (event: E) => void;
// 定义可复用的事件处理器类型
type ClickHandler = React.MouseEventHandler<HTMLButtonElement>;
type ChangeHandler = React.ChangeEventHandler<HTMLInputElement>;
type KeyDownHandler = React.KeyboardEventHandler<HTMLInputElement>;
function InputField({ onKeyDown }: { onKeyDown?: KeyDownHandler }) {
return <input onKeyDown={onKeyDown} />;
}
Hooks 类型
useState
// 基本类型 - 自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string
const [isActive, setIsActive] = useState(false); // boolean
// 可能为 null 的初始值
const [user, setUser] = useState<User | null>(null);
// 使用类型断言处理延迟初始化
const [items, setItems] = useState<Item[]>([]);
// 或
const [items, setItems] = useState<Item[] | null>(null);
// 延迟初始化(惰性初始化)
const [state, setState] = useState(() => {
const saved = localStorage.getItem("state");
return saved ? JSON.parse(saved) : defaultValue;
});
// 更新函数类型
const [count, setCount] = useState(0);
setCount(prev => prev + 1); // prev 类型自动推断为 number
useEffect
useEffect 通常不需要类型注解,但要注意返回值:
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// 清理函数返回 undefined 或函数
return () => clearInterval(timer);
}, []);
// 常见错误:箭头函数隐式返回
useEffect(() =>
setInterval(() => {}, 1000) // 错误!隐式返回 number
, []);
// 正确写法
useEffect(() => {
setInterval(() => {}, 1000);
// 没有返回值,或显式返回清理函数
}, []);
// 或使用 void 确保不返回值
useEffect(() => {
void setInterval(() => {}, 1000);
}, []);
useRef
// 1. DOM 元素引用 - 使用只读 ref
const inputRef = useRef<HTMLInputElement>(null);
// 使用时需要检查 null
if (inputRef.current) {
inputRef.current.focus();
}
// 使用非空断言(确保元素一定存在)
const inputRef = useRef<HTMLInputElement>(null!);
inputRef.current.focus(); // 不需要 null 检查
// 2. 可变值引用 - 使用泛型
const countRef = useRef<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// 组件挂载后赋值
useEffect(() => {
timerRef.current = setInterval(() => {
countRef.current++;
}, 1000);
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
useContext
import { createContext, useContext } from "react";
// 定义 context 类型
type Theme = {
mode: "light" | "dark";
toggle: () => void;
};
// 创建 context,必须提供默认值或 undefined
const ThemeContext = createContext<Theme | undefined>(undefined);
// Provider 组件
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = useState<"light" | "dark">("light");
const value = {
mode,
toggle: () => setMode(prev => prev === "light" ? "dark" : "light"),
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// 自定义 hook 获取 context
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
// 使用
function ThemedButton() {
const { mode, toggle } = useTheme();
return (
<button onClick={toggle} className={mode}>
切换主题
</button>
);
}
useReducer
使用判别联合类型定义 action:
type State = {
count: number;
error: string | null;
};
type Action =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: number }
| { type: "setError"; payload: string }
| { type: "clearError" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { ...state, count: state.count + action.payload };
case "decrement":
return { ...state, count: state.count - action.payload };
case "setError":
return { ...state, error: action.payload };
case "clearError":
return { ...state, error: null };
}
}
// 使用
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment", payload: 1 })}>
+
</button>
<button onClick={() => dispatch({ type: "decrement", payload: 1 })}>
-
</button>
</div>
);
}
useCallback 和 useMemo
// useCallback - 缓存函数
const handleClick = useCallback((id: string) => {
console.log(id);
}, []);
// 带参数类型的 useCallback
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 处理提交
},
[dependency]
);
// useMemo - 缓存计算值
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// 复杂类型推导
const userOptions = useMemo(() => {
return users.map(user => ({
value: user.id,
label: user.name,
}));
}, [users]); // 类型为 { value: string; label: string }[]
自定义 Hook
// 返回数组 - 使用 as const 或显式类型
function useToggle(initial: boolean) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
// 使用 as const 确保类型正确
return [value, { toggle, setTrue, setFalse }] as const;
}
// 使用时类型正确
const [isOpen, { toggle, setTrue, setFalse }] = useToggle(false);
// isOpen: boolean
// toggle: () => void
// 返回对象 - 类型更清晰
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// 使用
const { data, loading, error } = useFetch<User[]>("/api/users");
表单处理
受控组件
type FormData = {
username: string;
email: string;
age: number;
subscribe: boolean;
role: "admin" | "user";
};
function ControlledForm() {
const [formData, setFormData] = useState<FormData>({
username: "",
email: "",
age: 0,
subscribe: false,
role: "user",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox"
? (e.target as HTMLInputElement).checked
: type === "number"
? Number(value)
: value,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
<input
name="age"
type="number"
value={formData.age}
onChange={handleChange}
/>
<select name="role" value={formData.role} onChange={handleChange}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">提交</button>
</form>
);
}
非受控组件
function UncontrolledForm() {
const formRef = useRef<HTMLFormElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 获取表单数据
const formData = new FormData(formRef.current!);
console.log(Object.fromEntries(formData));
// 或直接访问 input
console.log(inputRef.current?.value);
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input ref={inputRef} name="username" defaultValue="默认值" />
<button type="submit">提交</button>
</form>
);
}
常见模式
高阶组件(HOC)
// 高阶组件类型定义
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function WithLoadingComponent(
props: P & { loading?: boolean }
) {
const { loading, ...rest } = props;
if (loading) {
return <div>加载中...</div>;
}
return <WrappedComponent {...rest as P} />;
};
}
// 使用
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading users={users} loading={isLoading} />;
Render Props
type MouseProps = {
render: (position: { x: number; y: number }) => React.ReactNode;
};
function Mouse({ render }: MouseProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
}
// 使用
<Mouse
render={({ x, y }) => (
<h1>鼠标位置: {x}, {y}</h1>
)}
/>
Compound Components
// 复合组件模式
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (id: string) => void;
} | null>(null);
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error("Tabs components must be used within Tabs");
}
return context;
}
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
);
}
function TabsList({ children }: { children: React.ReactNode }) {
return <div className="tabs-list">{children}</div>;
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useTabsContext();
const isActive = activeTab === id;
return (
<button
className={isActive ? "active" : ""}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
function TabsPanel({ id, children }: { id: string; children: React.ReactNode }) {
const { activeTab } = useTabsContext();
if (activeTab !== id) return null;
return <div className="tabs-panel">{children}</div>;
}
// 组合使用
Tabs.List = TabsList;
Tabs.Tab = Tab;
Tabs.Panel = TabsPanel;
function App() {
return (
<Tabs defaultTab="tab1">
<Tabs.List>
<Tabs.Tab id="tab1">标签 1</Tabs.Tab>
<Tabs.Tab id="tab2">标签 2</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="tab1">内容 1</Tabs.Panel>
<Tabs.Panel id="tab2">内容 2</Tabs.Panel>
</Tabs>
);
}
类型工具
提取组件 Props 类型
// 从组件提取 Props 类型
type ButtonProps = React.ComponentProps<typeof Button>;
// 从 JSX 元素提取 Props 类型
type DivProps = React.JSX.IntrinsicElements["div"];
// 提取并修改
type MyInputProps = Omit<React.ComponentProps<"input">, "onChange"> & {
onChange: (value: string) => void;
};
实用类型
// 使特定属性可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// 使特定属性必需
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// 示例
type User = {
id: number;
name: string;
email: string;
age?: number;
};
type UserUpdate = PartialBy<User, "id">; // id 可选
type UserCreate = Omit<User, "id">; // 创建时不需要 id
最佳实践总结
推荐做法
- 优先使用普通函数定义组件,避免使用
React.FC - 使用
React.ReactNode作为 children 类型 - 为事件处理器使用正确的事件类型
- 自定义 Hook 返回数组时使用
as const - Context 使用 undefined 作为默认值,并提供自定义 hook 进行访问
- 使用判别联合类型定义 reducer action
避免的做法
- 不要使用
any类型,至少使用unknown - 不要忽略 null 检查,特别是 ref 和可选属性
- 不要在 useEffect 中返回非函数值
- 不要过度使用类型断言,让 TypeScript 尽可能自动推断