【bug记录】 Argument of type ‘AsyncThunkAction<void, string, {}>‘ is not assignable to parameter of type
'# 【bug记录】 Argument of type ‘AsyncThunkAction<void, string, {}>‘ is not assignable to parameter of type
一、背景与问题
在基于 Redux Toolkit 构建的现代前端应用中,开发者常常会遇到 TypeScript 类型错误:
Argument of type 'AsyncThunkAction<void, string, {}>' is not assignable to parameter of type '(...args: any[]) => void'这个错误通常出现在使用 createAsyncThunk 定义的异步 action 与 dispatch 的调用场景不匹配时。它揭示了 Redux Toolkit 在类型推断和类型安全设计上的核心机制,也暴露了开发者在使用异步 action 时常见的类型配置陷阱。
二、基本原理
1. Redux Toolkit 的类型系统设计
Redux Toolkit 的 createAsyncThunk 是一个类型安全的异步 action 创建器,其核心设计基于以下类型定义:
type AsyncThunkAction<Returned, Pending = void, Rejected = void> =
| { type: 'pending', payload: Pending }
| { type: 'fulfilled', payload: Returned }
| { type: 'rejected', payload: Rejected, error: any }当使用 createAsyncThunk 时,开发者需要显式指定三个类型参数:
Returned: 异步操作成功时返回的数据类型Pending: 等待状态时的 payload 类型(可选,默认为void)Rejected: 异步操作失败时的 payload 类型(可选,默认为void)
2. 类型不匹配的根本原因
当调用 dispatch 时,TypeScript 会根据 dispatch 的参数类型进行类型校验。如果:
- 异步 action 返回的类型与 dispatch 的参数类型不匹配
- 使用了错误的泛型参数
- 未正确处理异步操作的返回值
就会触发类型错误。
三、环境准备
npm install @reduxjs/toolkit四、核心实现
1. 错误示例:类型不匹配
// loginSlice.ts
import { createAsyncThunk } from '@reduxjs/toolkit'
// 错误:未正确指定返回类型
export const login = createAsyncThunk('user/login', async (email: string) => {
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email }) })
return await response.json()
})
// 组件中调用
dispatch(login('test@example.com')) // 类型错误:预期返回类型为 string,实际为 void错误原因:createAsyncThunk 的第一个泛型参数未指定,导致默认为 void,而实际返回的是 string 类型。
2. 正确实现:显式指定返回类型
// loginSlice.ts
import { createAsyncThunk } from '@reduxjs/toolkit'
// 正确:显式指定返回类型为 string
export const login = createAsyncThunk<string, string>(
'user/login',
async (email: string) => {
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email }) })
return await response.json() // 返回 string 类型
}
)
// 组件中调用
dispatch(login('test@example.com')) // 类型正确关键点:
- 第一个泛型参数
string表示异步操作成功时返回的数据类型 - 第二个泛型参数
string表示异步操作的参数类型 - TypeScript 会根据这些类型进行严格的类型校验
3. 复杂类型场景:带错误处理的异步 action
// userSlice.ts
import { createAsyncThunk } from '@reduxjs/toolkit'
// 使用三个泛型参数
export const fetchUser = createAsyncThunk<
User,
string,
{ rejectValue: string }
>('user/fetchUser', async (userId: string) => {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return await response.json()
})
// 组件中调用
dispatch(fetchUser('123')).then((action) => {
if (action.type.endsWith('/fulfilled')) {
console.log('成功:', action.payload)
} else if (action.type.endsWith('/rejected')) {
console.log('失败:', action.payload)
}
})关键点:
- 第三个泛型参数用于指定错误处理的类型
rejectValue表示异步操作失败时返回的 payload 类型- 通过类型守卫可以精确判断 action 的类型
五、完整案例
1. 完整的登录功能实现
// loginSlice.ts
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
// 定义状态类型
interface LoginState {
status: 'idle' | 'loading' | 'succeeded' | 'failed'
user: User | null
error: string | null
}
// 定义用户类型
interface User {
id: string
name: string
email: string
}
// 创建异步 action
export const login = createAsyncThunk<User, string, { rejectValue: string }>(
'user/login',
async (email: string) => {
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email }) })
if (!response.ok) {
throw new Error('Login failed')
}
return await response.json()
}
)
// 创建 slice
const loginSlice = createSlice({
name: 'login',
initialState: {
status: 'idle',
user: null,
error: null
} as LoginState,
reducers: {
logout: (state) => {
state.status = 'idle'
state.user = null
state.error = null
}
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.status = 'loading'
state.error = null
})
.addCase(login.fulfilled, (state, action: PayloadAction<User>) => {
state.status = 'succeeded'
state.user = action.payload
state.error = null
})
.addCase(login.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload
})
}
})
export { login, loginSlice }// LoginComponent.tsx
import { useDispatch } from 'react-redux'
import { login } from './loginSlice'
const Login = () => {
const dispatch = useDispatch()
const handleLogin = () => {
dispatch(login('test@example.com'))
.then((action) => {
if (action.type.endsWith('/fulfilled')) {
console.log('登录成功:', action.payload)
} else if (action.type.endsWith('/rejected')) {
console.log('登录失败:', action.payload)
}
})
}
return (
<button onClick={handleLogin}>
登录
</button>
)
}六、源码解析
1. createAsyncThunk 的类型生成机制
// 简化版 createAsyncThunk 实现
function createAsyncThunk<Returned, Pending = void, Rejected = void>(
typePrefix: string,
payloadCreator: (
arg: any,
thunkAPI: {
dispatch: Dispatch<AsyncThunkAction<Returned, any, any>>
getState: () => RootState
requestId: string
requestStatus: 'pending' | 'fulfilled' | 'rejected'
}
) => Promise<Returned>
) {
// 生成具体类型
const actionCreator = (payload: any) => {
return {
type: `${typePrefix}/pending`,
payload
}
}
return actionCreator
}2. 类型校验的关键点
createAsyncThunk返回的 actionCreator 会生成三种类型:pending/fulfilled/rejected- TypeScript 会根据传入的泛型参数进行类型校验
- dispatch 的参数类型必须与 actionCreator 的返回类型匹配
七、进阶使用
1. 使用泛型参数优化类型
// 使用泛型参数避免重复定义
type AuthAction = AsyncThunkAction<AuthState, string, { rejectValue: string }>
export const login = createAsyncThunk<AuthState, string, { rejectValue: string }>(
'auth/login',
async (email: string) => {
// ...
}
)2. 自定义类型守卫
function isFulfilledAction(action: any): action is { type: string, payload: AuthState } {
return action.type.endsWith('/fulfilled')
}3. 使用 TypeScript 的类型推断
// 不需要显式指定泛型参数
export const login = createAsyncThunk(
'user/login',
async (email: string) => {
// ...
}
)八、性能与工程实践
1. 性能优化策略
- 使用
thunk的防抖/节流 - 避免不必要的异步 action 调用
- 使用
useSelector的 memoization
// 使用 useSelect 的 memoization
const user = useSelector((state: RootState) => state.login.user)2. 安全性注意事项
- 检查服务器响应状态码
- 处理网络错误和超时
- 避免直接暴露敏感数据
// 安全的 fetch 调用
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email })
})
if (!response.ok) {
throw new Error('Network response was not ok')
}3. 异步 action 的并发控制
// 使用 redux-thunk 的并发控制
export const login = createAsyncThunk(
'user/login',
async (email: string) => {
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email }) })
return await response.json()
}
)九、常见问题与踩坑
1. 常见错误场景
| 场景 | 错误示例 | 解决方案 |
|---|---|---|
| 类型不匹配 | dispatch(login('test')) | 显式指定泛型参数 |
| 错误处理缺失 | dispatch(login()) | 添加错误处理逻辑 |
| 未正确处理异步返回值 | dispatch(login()) | 使用 .then() 或 .catch() |
| 未使用类型守卫 | if (action.type === 'user/login/fulfilled') | 使用类型守卫判断 action 类型 |
2. 典型错误案例
// 错误:未处理错误情况
dispatch(login('test@example.com')).catch((err) => {
console.error(err)
})改进方案:
dispatch(login('test@example.com'))
.then((action) => {
if (action.type.endsWith('/fulfilled')) {
console.log('成功:', action.payload)
} else {
console.log('失败:', action.payload)
}
})十、最佳实践
1. 推荐的使用场景
- 需要处理异步操作的复杂逻辑
- 需要返回具体的数据类型
- 需要处理错误情况
- 需要进行类型安全的 dispatch
2. 不推荐的使用场景
- 简单的同步操作
- 需要频繁调用的简单 action
- 不需要返回具体数据的场景
- 无需处理错误的场景
3. 类型安全的最佳实践
- 显式指定所有泛型参数
- 使用类型守卫判断 action 类型
- 使用
PayloadAction处理 action 的 payload - 使用
createSlice管理 state 变化 - 使用
useSelector进行状态订阅
十一、总结
"Argument of type 'AsyncThunkAction<...>' is not assignable to parameter of type..." 这个错误揭示了 Redux Toolkit 在类型安全设计上的核心机制。通过深入理解 createAsyncThunk 的类型系统,我们可以更好地避免类型错误,提高代码的可维护性和可读性。
在实际开发中,我们应该:
- 显式指定所有泛型参数
- 使用类型守卫判断 action 类型
- 合理使用
PayloadAction处理 payload - 理解不同场景下的使用规范
同时也要注意:
- 避免在简单场景中过度使用异步 action
- 正确处理异步操作的错误和返回值
- 合理使用类型推断减少冗余
通过这些实践,我们可以构建出更加健壮、类型安全的 Redux 应用。
评论已关闭