在 React 与 TypeScript 结合的项目中,正确标注事件对象、Ref 和 Context 的类型,能显著提升代码的健壮性和可维护性。本节聚焦这三个高频场景的类型定义与常见问题。
18.3.1 事件对象的类型
React 中的事件是合成事件(SyntheticEvent),并非原生 DOM 事件。TypeScript 为每种事件提供了对应的泛型类型,使用时需要指定触发事件的元素类型。
常用事件类型速查
| 事件处理器 | 事件对象类型 | 典型使用场景 |
|------------|-------------|--------------|
| onClick | React.MouseEvent<HTMLButtonElement> | 按钮点击 |
| onChange | React.ChangeEvent<HTMLInputElement> | 输入框内容变化 |
| onSubmit | React.FormEvent<HTMLFormElement> | 表单提交 |
| onKeyDown | React.KeyboardEvent<HTMLInputElement> | 键盘按键 |
| onFocus | React.FocusEvent<HTMLInputElement> | 获取焦点 |
| onScroll | React.UIEvent<HTMLDivElement> | 滚动事件 |
泛型参数中的元素类型可以从 DOM 元素标签对应推导:<input> 对应 HTMLInputElement,<button> 对应 HTMLButtonElement,<div> 对应 HTMLDivElement。
实际开发示例
1. 点击事件
function Button() {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget); // 当前绑定的元素
console.log(e.target); // 实际触发事件的元素
};
return <button onClick={handleClick}>点击</button>;
}
2. 输入框 onChange 事件
function Input() {
const [value, setValue] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value); // e.target 被正确推断为 HTMLInputElement
};
return <input value={value} onChange={handleChange} />;
}
如果事件处理器是内联的,TypeScript 可以自动推断类型,无需显式标注:
<input onChange={(e) => setValue(e.target.value)} />; // e 自动推断
3. 表单提交事件
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 通过 FormData 获取表单数据
const formData = new FormData(e.currentTarget);
};
return <form onSubmit={handleSubmit}>...</form>;
}
4. 处理多种事件类型(联合类型)
当同一个函数需要处理多个事件类型时,可使用联合类型:
function handleInputOrClick(
e: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>
) {
// 通过类型收窄(typeof e.type)区分处理逻辑
}
常见错误与解决
❌ 错误:遗漏泛型参数,使用更宽泛的类型
const handleChange = (e: React.ChangeEvent) => { ... }; // 缺少元素类型
导致 e.target.value 可能报错,因为 target 被推断为通用的 EventTarget。
✅ 正确:明确指定元素类型
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... };
❌ 错误:混用原生事件与 React 事件
const handleClick = (e: MouseEvent) => { ... }; // 这是原生 MouseEvent,缺少 React 的合成特性
React 事件系统的 stopPropagation、persist 等方法在原生事件上不可用或行为不一致。
18.3.2 Ref 的类型定义
Ref 在 React 中用于引用 DOM 元素或存储跨渲染周期的可变值。TypeScript 需要精确的类型定义来确保 ref 的正确使用。
1. useRef 的基本类型
useRef 的类型会根据初始值自动推断:
// 不可变的值引用(不绑定 DOM)
const countRef = useRef<number>(0); // countRef.current 的类型为 number
// DOM 元素引用,初始值为 null,需要联合类型
const inputRef = useRef<HTMLInputElement>(null); // inputRef.current 的类型为 HTMLInputElement | null
DOM ref 的常见用法:
function InputWithFocus() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus(); // 可选链,因为 current 可能为 null
};
return (
<>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦输入框</button>
</>
);
}
2. 使用 Ref 保存可变值(非 DOM)
当 ref 用于存储任意可变值(如定时器 ID、上一次的 props),显式指定类型即可:
const timerRef = useRef<number | null>(null);
useEffect(() => {
timerRef.current = window.setInterval(() => { ... }, 1000);
return () => clearInterval(timerRef.current!);
}, []);
3. forwardRef 的类型定义
当需要将 ref 转发给子组件内的 DOM 节点时,使用 forwardRef,并明确传入 ref 的泛型参数。
子组件:
interface InputProps {
label: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(({ label }, ref) => {
return (
<label>
{label}
<input ref={ref} />
</label>
);
});
forwardRef 接受两个泛型参数:
- 第一个是 ref 指向的 DOM 元素类型(如
HTMLInputElement) - 第二个是 组件的 Props 类型
父组件使用:
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <Input label="姓名" ref={inputRef} />;
}
4. 回调 Ref 的类型
回调 ref 可以更精细地控制 ref 的赋值时机,类型标注如下:
function Example() {
const setRef = (node: HTMLDivElement | null) => {
// 当组件挂载时 node 为元素,卸载时为 null
if (node) {
console.log(node.getBoundingClientRect());
}
};
return <div ref={setRef}>内容</div>;
}
5. useImperativeHandle 暴露方法的类型
结合 forwardRef 和 useImperativeHandle,子组件可以向父组件暴露自定义方法。此时需要定义一个接口约定暴露的方法。
// 定义暴露的方法类型
interface InputHandle {
focus: () => void;
clear: () => void;
}
const Input = forwardRef<InputHandle, InputProps>(({ label }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = '';
},
}));
return <input ref={inputRef} />;
});
// 父组件使用
function Parent() {
const inputRef = useRef<InputHandle>(null);
return (
<>
<Input label="" ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.clear()}>清空</button>
</>
);
}
关键点:
forwardRef的泛型第一个参数改为InputHandleuseImperativeHandle返回的对象必须符合InputHandle接口- 父组件使用
useRef<InputHandle>(null)
18.3.3 Context 的类型定义
Context 用于跨层级传递数据,TypeScript 需要明确 Context 的类型,避免消费时出现类型错误。
1. 创建 Context
使用 createContext 时传入默认值,TypeScript 会自动推断类型;若初始值无法覆盖所有场景,需显式声明类型。
方案一:提供有意义的默认值(推荐)
interface Theme {
color: string;
fontSize: number;
}
const defaultTheme: Theme = {
color: '#000',
fontSize: 14,
};
const ThemeContext = createContext<Theme>(defaultTheme);
// 提供者
function App() {
return (
<ThemeContext.Provider value={{ color: '#333', fontSize: 16 }}>
<Child />
</ThemeContext.Provider>
);
}
方案二:初始值可能为 undefined 时
有些 Context 在未提供 Provider 时可能为 undefined,需要联合类型:
interface User {
name: string;
age: number;
}
const UserContext = createContext<User | undefined>(undefined);
消费时必须进行空检查:
function Child() {
const user = useContext(UserContext);
if (!user) {
throw new Error('Child must be used within UserContext.Provider');
// 或 return <div>请先登录</div>
}
return <div>{user.name}</div>;
}
更优雅的方式:封装自定义 Hook
避免每次消费都空检查,可以封装一个 hook:
function useUser() {
const user = useContext(UserContext);
if (user === undefined) {
throw new Error('useUser must be used within UserContext.Provider');
}
return user;
}
// 直接使用,无需空检查
function Child() {
const user = useUser();
return <div>{user.name}</div>;
}
2. 复杂 Context 与状态管理
Context 常结合 useReducer 或 useState 进行全局状态管理,类型定义需覆盖状态和更新函数。
// 定义 state 类型
interface CounterState {
count: number;
}
// 定义 action 类型(联合类型)
type CounterAction =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'ADD'; payload: number };
// Context 包含 state 和 dispatch
const CounterContext = createContext<{
state: CounterState;
dispatch: React.Dispatch<CounterAction>;
} | undefined>(undefined);
// 自定义 Provider
function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// 自定义消费 Hook
function useCounter() {
const context = useContext(CounterContext);
if (!context) {
throw new Error('useCounter must be used within CounterProvider');
}
return context;
}
使用时类型安全且简洁:
function Counter() {
const { state, dispatch } = useCounter();
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
</div>
);
}
小结
- 事件对象:使用
React.ChangeEvent<HTMLInputElement>等泛型,记得指明元素类型。 - Ref:
- DOM ref 推荐
useRef<HTMLInputElement>(null),读取时使用可选链。 forwardRef需要两个泛型参数(ref 类型、Props 类型)。- 暴露方法时定义接口,并在
forwardRef和useImperativeHandle中使用。 - Context:
- 创建时提供清晰类型,不确定时联合
undefined。 - 封装自定义 Hook 进行空检查和错误提示,提升使用体验。
这些类型定义能让你的 React 代码在 TypeScript 的保护下更加可靠,减少运行时错误,同时提高代码的自我描述能力。