TypeScript 与 React 结合时,大部分基础类型可以自动推断。但在 hooks 使用中,明确标注类型能让代码更健壮、减少运行时错误,尤其是在初始值为 null 或复杂对象时。本节聚焦实战中高频出现的 hooks 类型写法。
useState 的类型标注
useState 支持泛型,但很多情况下 TypeScript 能根据初始值自动推断。需要显式标注的场景包括:初始值为 null 或 undefined、数据类型较复杂、联合类型等。
1. 基础类型自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [visible, setVisible] = useState(true); // boolean
无需手动标注,类型已明确。
2. 初始值为 null 或 undefined 时的联合类型
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 元素有对应的类型:HTMLDivElement、HTMLButtonElement、HTMLTextAreaElement 等。
2. 存储可变值(不触发重渲染)
泛型应指定值的类型,且必须传入初始值。此时返回的 RefObject 为 MurableRefObject,current 可读写。
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 的内置事件类型(
ChangeEvent、FormEvent等)。
掌握这些模式,你就能在项目中对 Hooks 进行高效、安全的类型标注。