如何让TypeScript识别异步API请求捕获的Response类型错误以避免类型断言?
我有一些通过fetch API和Promise实现的API方法(比如下面的login方法):
export const handleResponse = async (response: Response, session?: Session): Promise<Response> => { // 检查错误响应 if (!response.ok) { if ([401, 403].includes(response.status) && session?.auth) { session?.logout(); } return Promise.reject(response); } return response; } export const login = async (username: string, password: string) => { const response = await fetch("/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); return (await handleResponse(response)); }
我用await语法调用这些方法:
try { const response = await API.client.login(username, password); console.log(await response.json()); // 处理响应 } catch (error) { console.log(await error.json()); // 处理错误 }
但TypeScript无法识别catch里error参数的类型,默认把它标记为unknown,导致我必须用类型断言才能访问它的属性(比如.json()方法),这很麻烦。
如何解决这个问题?
这个问题我之前处理过类似的场景,核心原因是TypeScript默认会把Promise reject的内容推断为unknown类型,所以你在catch块里拿不到明确的类型提示。下面给你几个实用的解决方案,都能避免繁琐的类型断言:
方案一:自定义API错误类(最推荐)
我们可以创建一个专门的错误类,把Response对象封装进去,这样TypeScript就能明确知道catch到的错误类型:
首先定义错误类:
// 自定义API错误类,携带响应对象 class ApiError extends Error { constructor(public response: Response) { super('API请求失败'); // 修复ES6错误类的原型链问题 Object.setPrototypeOf(this, ApiError.prototype); } }
然后修改handleResponse函数,把原来的Promise.reject(response)改成抛出这个错误类的实例:
export const handleResponse = async (response: Response, session?: Session): Promise<Response> => { if (!response.ok) { if ([401, 403].includes(response.status) && session?.auth) { session?.logout(); } // 抛出自定义错误,替代Promise.reject throw new ApiError(response); } return response; }
最后在调用的时候,用类型守卫判断错误类型:
try { const response = await API.client.login(username, password); console.log(await response.json()); // 处理响应 } catch (error) { // 用instanceof做类型守卫,TypeScript会自动推断error的类型 if (error instanceof ApiError) { console.log(await error.response.json()); // 这里可以安全访问response的所有属性,不需要断言 } else { // 处理其他意外错误,比如网络故障 console.error('发生未知错误:', error); } }
这种方式的好处是类型安全,还能在错误类里扩展更多属性(比如错误码、错误信息),非常灵活。
方案二:提前解析错误响应并抛出结构化对象
如果不想自定义错误类,也可以在handleResponse里直接解析错误响应,抛出一个结构化的对象,让TypeScript明确它的类型:
首先定义错误响应的类型:
// 根据你的API实际返回的错误结构定义 interface ApiErrorData { message: string; code?: number; // 其他错误字段 } interface RequestError { status: number; data: ApiErrorData; }
然后修改handleResponse:
export const handleResponse = async (response: Response, session?: Session): Promise<Response> => { if (!response.ok) { if ([401, 403].includes(response.status) && session?.auth) { session?.logout(); } // 提前解析错误响应,抛出结构化对象 const errorData = await response.json() as ApiErrorData; throw { status: response.status, data: errorData } as RequestError; } return response; }
调用时可以用类型守卫或者直接断言(类型守卫更安全):
try { const response = await API.client.login(username, password); console.log(await response.json()); } catch (error) { // 用类型守卫判断是否是我们抛出的RequestError if (typeof error === 'object' && error !== null && 'status' in error && 'data' in error) { const requestError = error as RequestError; console.log(requestError.status, requestError.data.message); } else { console.error('未知错误:', error); } }
这种方式省去了自定义类的步骤,适合简单场景,但类型守卫的代码会稍微繁琐一点。
方案三:给Promise指定完整的类型(不推荐,灵活性差)
如果你不想修改现有逻辑,也可以给login函数明确指定返回的Promise类型,把reject的类型也声明出来:
// 明确指定login返回的Promise,resolve是Response,reject也是Response export const login = async (username: string, password: string): Promise<Response> => { const response = await fetch("/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); return (await handleResponse(response)); }
然后在catch里用类型断言,但其实这只是让你心里有数,TypeScript还是会把error视为unknown,所以你还是需要手动断言:
catch (error) { // 这里必须用断言,还是有点繁琐,所以不推荐 console.log(await (error as Response).json()); }
这种方式本质上还是用了类型断言,只是提前声明了类型,不如前两种方案优雅。
总结一下,最推荐的是第一种自定义错误类的方式,既保证类型安全,又能灵活扩展错误信息,完全避免了繁琐的类型断言。
备注:内容来源于stack exchange,提问作者moltarze

