如何使用 TypeScript 编写 API
客户端应用程序的开发人员每天与API打交道。根据操作的成功与否或业务逻辑,标准化API响应是一个好习惯。通常,响应包括状态、错误等标准字段。
有了这些标准字段,开发人员可以对操作的状态做出反应,并构建与应用程序的进一步用户交互。如果注册成功,应该关闭表单并显示成功消息。然而,如果数据格式不正确,验证错误应该在表单中显示。
这就提出了如何在项目中方便、快速和灵活地描述响应类型的问题。
遇到的问题
有时,项目中的响应类型仅用一种类型描述,带有多个可选参数。在大多数情况下,这可能足够了,TypeScript在编写代码时会建议这些参数,但需要额外检查这些参数的存在。下面是一个这样的类型的示例:
export enum ApiStatus {
OK = `ok`,
ERROR = `error`,
FORM_ERRORS = `form_errors`,
REDIRECT = `redirect`,
}
export type ApiData = {
status: ApiStatus
error?: string
errors?: Record<string, string>
url?: string
}
这种方法的唯一优点是它的简单性。我们可以将ApiData类型添加到任何响应类型中,这样就足够了。
export type UserProfile = {
id: number
name: string
last_name: string
birthday: string
city: string
}
export type UserProfileResponse = ApiData & {
user: UserProfile
}
// to simulate an API call
const updateProfileAPI = async(data: Partial<UserProfile>): Promise<UserProfileResponse> => {
return Promise.resolve({} as UserProfileResponse)
}
然而,我认为这种单一的优势被一个显著的劣势所抵消。这种方法的缺点是缺乏透明度。
此外,通过向响应类型添加这样的类型,你永远无法确切知道特定请求的响应将是什么。想象一下,对于一个POST请求,你可以从API获得有限数量的响应场景。
场景可能是以下之一:
- 成功操作,状态为’ok’和一些数据;
- 验证错误,状态为’form_errors’和错误:[{}, {}],就这样。
这意味着在这种情况下,你永远不会有状态为’redirect’作为可能的响应场景。同样,你为什么需要一个错误参数来响应GET请求呢?
结果表明,我们不能仅仅通过查看响应类型就了解我们确切的响应选项。要了解所有可能的响应变体,你需要打开执行请求和处理响应的函数的代码。
响应类型的实用类型
上述缺点可以通过自定义实用类型来解决。每个场景都有单独的类型:成功操作、服务器错误、验证错误或强制重定向。
这些类型可以单独使用或组合使用,以反映特定响应的所有可能响应选项。每种类型都有一个通用类型,允许传递对应于该响应的数据类型的数据。
export enum ApiStatus {
OK = `ok`,
ERROR = `error`,
FORM_ERRORS = `form_errors`,
REDIRECT = `redirect`,
}
export type ApiSuccess<T extends Record<string, unknown> | unknown = unknown> = T & {
status: ApiStatus.OK,
}
export type ApiError<T extends Record<string, unknown> = { error: string } > = T & {
status: ApiStatus.ERROR,
}
export type ApiFormErrors<T extends Record<string, unknown> = { errors: Record<string, string> }> = T & {
status: ApiStatus.FORM_ERRORS,
}
export type ApiRedirect<T extends Record<string, unknown> = { url: string }> = T & {
status: ApiStatus.REDIRECT,
}
export type ApiResponse<T extends Record<string, unknown> | unknown = unknown, K extends Record<string, unknown> = { error: string }, R extends Record<string, unknown> = { errors: Record<string, string> }> = ApiSuccess<T> | ApiError<K> | ApiFormErrors<R>
此外,我还创建了一个通用的ApiResponse
类型,它包括几个实用类型。这将节省为每个POST请求添加所有场景的时间。
以下是针对不同场景使用这些实用类型的示例:
export type FetchUserProfile = ApiSuccess<{
user: UserProfile
}>
export type FetchUserConfig = ApiSuccess<{
config: Record<string, string | number | boolean>
}> | ApiError
export type AddUserSocialNetworkAsLoginMethod = ApiResponse<{
social_network: string,
is_active: boolean
}, { message: string }> | ApiRedirect<{ redirect_url: string }>
实际差异
下面是一个用户个人资料的类型示例,以及用户个人资料更新功能返回的响应类型。
const updateProfile = async(): Promise<void> => {
try {
const data = await updateProfileAPI({ name: 'New name' })
// [!!!] Typescript does not highlight that the 'user' property could not exist on the 'data' property
// In the case when data.status === ApiStatus.ERROR|FORM_ERRORS|REDIRECT
console.log(data.user.id)
if (data.status === ApiStatus.OK) {
updatedProfileState(data.user)
return
}
if (data.status === ApiStatus.ERROR) {
// Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
// Type 'undefined' is not assignable to type 'string'.
showNotification('danger', data.error)
return
}
if (data.status === ApiStatus.FORM_ERRORS) {
// Argument of type 'Record<string, string> | undefined' is not assignable to parameter of type 'Record<string, string>'.
// Type 'undefined' is not assignable to type 'Record<string, string>'.
showValidationErrors(data.errors)
return
}
if (data.status === ApiStatus.REDIRECT) {
// Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
// Type 'undefined' is not assignable to type 'string'.
redirect(data.url)
return
}
throw new Error('Something went wrong...')
} catch (err) {
console.error('User: updateProfile - ', err)
}
}
这是一张TypeScript如何检查这段代码的图片:
在图片中,你可以看到TypeScript突出显示了一些标准响应的预期值,如error、errors或url。这是因为linter(代码检查器)认为这些值可能是未定义的。这个问题可以通过在状态检查时增加额外的检查来轻松解决,但它已经展示了这种方法的问题。
另外,请注意,在console.log(data.user.id)这一行中,user值没有被突出显示为可能未定义。如果我们收到的响应类型不是成功的,这是我们会遇到的情况。
使用ApiResponse等实用类型,我们就不会遇到这样的问题。
export type UserProfileResponseV2 = ApiResponse<{
user: UserProfile
}> | ApiRedirect
const newUpdateProfileAPI = async(data: Partial<UserProfile>): Promise<UserProfileResponseV2> => {
return Promise.resolve({} as UserProfileResponseV2)
}
这是一张展示TypeScript如何对这段代码进行lint检查的图片:
在这种情况下,一切工作正常进行:
TypeScript理解对于相应的状态,将会有相应的标准字段。
它指出,在除了成功的响应之外的所有响应类型中,user值可能是未定义的。然而,在检查响应的成功状态后,这个值不再被突出显示,并且是已定义的。
结论
在项目中实现这些实用类型后,开发人员体验显著提升。现在,类型完全对应于API可以提供的可能响应场景。
这也有助于避免在某些响应类型中不可用值的使用潜在错误,就像user值的例子一样。
此外,不需要查看代码中的响应处理实现就能理解实际的响应类型。你可以立即看到完整的情况。
如果你对这些实用类型是如何工作的感兴趣,你可以查看TypeScript Playground页面。
如何找到更多同类API?
幂简集成是国内领先的API集成管理平台,专注于为开发者提供全面、高效、易用的API集成解决方案。幂简API平台可以通过以下两种方式找到所需API:通过关键词搜索API、或者从API Hub分类页进入寻找。
原文链接:https://itnext.io/how-to-write-api-response-types-with-typescript-f8152ddd43dd