人人都会AI编程

18.3 事件对象、Ref、Context 的类型定义

更新时间:2026-07-11

在 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 暴露方法的类型

结合 forwardRefuseImperativeHandle,子组件可以向父组件暴露自定义方法。此时需要定义一个接口约定暴露的方法。

// 定义暴露的方法类型
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 的泛型第一个参数改为 InputHandle
  • useImperativeHandle 返回的对象必须符合 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 常结合 useReduceruseState 进行全局状态管理,类型定义需覆盖状态和更新函数。

// 定义 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 类型)。
  • 暴露方法时定义接口,并在 forwardRefuseImperativeHandle 中使用。
  • Context
  • 创建时提供清晰类型,不确定时联合 undefined
  • 封装自定义 Hook 进行空检查和错误提示,提升使用体验。

这些类型定义能让你的 React 代码在 TypeScript 的保护下更加可靠,减少运行时错误,同时提高代码的自我描述能力。