人人都会AI编程

18.2 Hooks 类型标注与泛型使用

更新时间:2026-07-11

TypeScript 与 React 结合时,大部分基础类型可以自动推断。但在 hooks 使用中,明确标注类型能让代码更健壮、减少运行时错误,尤其是在初始值为 null 或复杂对象时。本节聚焦实战中高频出现的 hooks 类型写法。

useState 的类型标注

useState 支持泛型,但很多情况下 TypeScript 能根据初始值自动推断。需要显式标注的场景包括:初始值为 nullundefined、数据类型较复杂、联合类型等。

1. 基础类型自动推断

const [count, setCount] = useState(0);       // number
const [name, setName] = useState('');        // string
const [visible, setVisible] = useState(true); // boolean

无需手动标注,类型已明确。

2. 初始值为 nullundefined 时的联合类型

interface User {
  id: number;
  name: string;
}

const [user, setUser] = useState<User | null>(null);

必须显式传入泛型 <User | null>,否则 TypeScript 会将 user 推断为 null,后续赋值会报错。

3. 对象数组的类型标注

interface Product {
  id: number;
  title: string;
  price: number;
}

const [products, setProducts] = useState<Product[]>([]);

如果不标注 <Product[]>,空数组会被推断为 never[],导致 push 或其他赋值操作报类型不匹配。

4. 惰性初始化和函数式更新

const [list, setList] = useState<string[]>(() => {
  const saved = localStorage.getItem('list');
  return saved ? JSON.parse(saved) : [];
});

// 函数式更新时,回调参数类型自动推断
setList(prev => [...prev, 'new item']);

useEffect 的类型标注

useEffect 的回调函数和依赖数组类型通常自动处理,无需额外标注。唯一需要注意的是清理函数的返回值应当是 void,不要返回其他类型(异步函数需额外处理)。

useEffect(() => {
  const timer = setInterval(() => { /* ... */ }, 1000);
  return () => clearInterval(timer); // 清理函数必须是 void 或 () => void
}, []);

如果需要在 useEffect 中执行异步操作,不能直接把回调声明为 async,因为 async 函数返回 Promise<void>,而清理函数期望普通函数。正确做法是在内部定义 async 函数并立即调用:

useEffect(() => {
  const fetchData = async () => {
    const res = await api.getData();
    setData(res);
  };
  fetchData();
}, []);

useReducer 的类型标注

useReducer 涉及状态类型和动作类型,通常需要手动定义以确保动作派发时类型安全。

// 定义动作类型
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number };

interface State {
  count: number;
}

const initialState: State = { count: 0 };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialState);
// dispatch 参数类型自动根据 reducer 推导,安全且精确
dispatch({ type: 'reset', payload: 10 });   // 正确
dispatch({ type: 'reset', payload: '10' }); // 报错:payload 必须是 number

useRef 的类型标注

useRef 的用途分两类场景,类型标注方式不同。

1. 作为 DOM 元素引用
需要传入泛型并初始化为 null,该 ref 会成为只读的 RefObject

const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
  inputRef.current?.focus(); // 安全访问
}, []);

不同类型的 HTML 元素有对应的类型:HTMLDivElementHTMLButtonElementHTMLTextAreaElement 等。

2. 存储可变值(不触发重渲染)
泛型应指定值的类型,且必须传入初始值。此时返回的 RefObjectMurableRefObjectcurrent 可读写。

const intervalRef = useRef<number | null>(null);

useEffect(() => {
  intervalRef.current = window.setInterval(() => { ... }, 1000);
  return () => clearInterval(intervalRef.current!);
}, []);

如果初始值为 null,类型应显式联合 null,如 useRef<number | null>(null)

useMemo 与 useCallback 的类型标注

这两个 hooks 的类型通常由返回值自动推断,无需手动指定泛型。除非你在工厂函数中返回了复杂的联合类型,或希望明确声明以增加文档可读性。

// 自动推断为 number
const memoizedValue = useMemo(() => expensiveCompute(a, b), [a, b]);

// 自动推断为 (id: number) => void
const handleClick = useCallback(
  (id: number) => {
    fetchDetail(id);
  },
  [fetchDetail]
);

若需要手动约束,可以传入泛型:

const memoizedValue = useMemo<number>(() => compute(a), [a]);
const handleClick = useCallback<(id: number) => void>(
  (id) => { fetchDetail(id); },
  [fetchDetail]
);

但通常多余,让 TypeScript 推导即可。

useContext 的类型标注

使用 createContext 时必须指定泛型,并且要提供合理的默认值,否则消费时类型可能为 undefined,导致强制检查。

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

// 消费时强制检查
function ThemedButton() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('必须在ThemeProvider内使用');
  const { theme, toggleTheme } = context;
  // ...
}

更推荐在创建 Context 时就提供安全的默认值,避免每次都判空:

const ThemeContext = createContext<ThemeContextType>({
  theme: 'light',
  toggleTheme: () => {},
});
// 后续可以直接使用,无需判空

自定义 Hooks 的泛型使用

自定义 Hook 经常需要处理通用逻辑,此时可将泛型参数传递给 Hook,以保持类型灵活性。

示例:封装一个通用的 useArray

function useArray<T>(initialArray: T[]) {
  const [array, setArray] = useState<T[]>(initialArray);

  const push = (element: T) => {
    setArray(prev => [...prev, element]);
  };

  const remove = (index: number) => {
    setArray(prev => prev.filter((_, i) => i !== index));
  };

  return { array, push, remove };
}

// 使用
const { array: numbers, push } = useArray<number>([1, 2, 3]);
push(4); // 类型安全

const { array: users, push: addUser } = useArray<User>([]);
addUser({ id: 1, name: 'Alice' }); // 正确

示例:带参数的 useDebounce

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

// 使用:自动推断 value 类型为 string
const debouncedSearch = useDebounce(searchTerm, 500);

当你的自定义 Hook 需要使用外部传入的数据类型时,泛型是不可或缺的工具。

常见事件处理与异步状态

在 Hooks 中处理表单、点击等事件时,React 提供了内置的事件类型:

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setText(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // ...
};

异步请求的状态也值得为 useState 定义联合类型:

interface AsyncState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useAsync<T>(asyncFn: () => Promise<T>) {
  const [state, setState] = useState<AsyncState<T>>({
    data: null,
    loading: true,
    error: null,
  });
  // ...实现逻辑
  return state;
}

小结

在 React + TypeScript 中,Hooks 的类型标注并不复杂,记住几个关键规则:

  • useState:初始值为 null/undefined 或复杂结构时显式传泛型。
  • useReducer:强类型 Action 联合类型是核心。
  • useRef:分清楚 DOM 引用和可变值,初始值与泛型要匹配。
  • useContext:创建 Context 时提供明确的泛型和合理的默认值。
  • 自定义 Hooks:善于利用泛型提升逻辑复用性。
  • 事件处理使用 React 的内置事件类型(ChangeEventFormEvent 等)。

掌握这些模式,你就能在项目中对 Hooks 进行高效、安全的类型标注。