跳到主要内容

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 转换,无需手动导入 React
  • isolatedModules: 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

最佳实践总结

推荐做法

  1. 优先使用普通函数定义组件,避免使用 React.FC
  2. 使用 React.ReactNode 作为 children 类型
  3. 为事件处理器使用正确的事件类型
  4. 自定义 Hook 返回数组时使用 as const
  5. Context 使用 undefined 作为默认值,并提供自定义 hook 进行访问
  6. 使用判别联合类型定义 reducer action

避免的做法

  1. 不要使用 any 类型,至少使用 unknown
  2. 不要忽略 null 检查,特别是 ref 和可选属性
  3. 不要在 useEffect 中返回非函数值
  4. 不要过度使用类型断言,让 TypeScript 尽可能自动推断

参考资料