TypeScript实现类型安全JSON:API解析器:返回类型随参数确定
类型安全解析JSON:API的data字段
JSON:API规范里的data字段可能是单个资源对象,也可能是资源对象数组,要写出类型安全的解析函数,避免返回T | T[]这种模糊的联合类型,这里有几个靠谱的TypeScript解决方案:
方案1:用函数重载自动推断类型
先定义好JSON:API的基础结构,再通过函数重载区分单个资源和数组资源的场景,TypeScript会自动根据输入的响应类型推导返回值类型:
// 定义JSON:API的单个资源结构 interface JsonApiResource<TAttrs> { type: string; id: string; attributes: TAttrs; } // 单个资源的响应格式 interface SingleResourceResponse<TAttrs> { data: JsonApiResource<TAttrs>; } // 资源数组的响应格式 interface ResourceArrayResponse<TAttrs> { data: JsonApiResource<TAttrs>[]; } // 最终要返回的目标类型:合并id、type和attributes的所有字段 type Resource<TAttrs> = { id: string; type: string; } & TAttrs; // 函数重载签名:先声明两种输入对应的返回类型 function parseData<T>(response: SingleResourceResponse<T>): Resource<T>; function parseData<T>(response: ResourceArrayResponse<T>): Resource<T>[]; // 函数实现 function parseData<T>( response: SingleResourceResponse<T> | ResourceArrayResponse<T> ): Resource<T> | Resource<T>[] { if (Array.isArray(response.data)) { return response.data.map(item => ({ id: item.id, type: item.type, ...item.attributes })); } else { return { id: response.data.id, type: response.data.type, ...item.attributes }; } }
使用示例
// 单个文章的响应数据 const singleArticle: SingleResourceResponse<{ name: string; price: number }> = { data: { type: 'article', id: 'article_id_1', attributes: { name: 'article_1', price: 99 } } }; // TypeScript自动推断singleResult是单个Resource类型 const singleResult = parseData(singleArticle); console.log(singleResult.price); // 类型安全,无报错 // 文章数组的响应数据 const articleList: ResourceArrayResponse<{ name: string; price: number }> = { data: [ { type: 'article', id: '1', attributes: { name: 'a1', price: 99 } }, { type: 'article', id: '2', attributes: { name: 'a2', price: 199 } } ] }; // TypeScript自动推断articleListResult是Resource数组类型 const articleListResult = parseData(articleList); console.log(articleListResult[0].name); // 类型安全,无报错
方案2:用泛型参数显式指定是否返回数组
如果需要调用者明确控制返回类型,可以加一个布尔类型的泛型参数,通过条件类型来决定返回值:
interface JsonApiResource<TAttrs> { type: string; id: string; attributes: TAttrs; } type Resource<TAttrs> = { id: string; type: string } & TAttrs; // 根据IsArray参数决定响应的data类型 type JsonApiResponse<TAttrs, IsArray extends boolean> = IsArray extends true ? { data: JsonApiResource<TAttrs>[] } : { data: JsonApiResource<TAttrs> }; function parseData<TAttrs, IsArray extends boolean = false>( response: JsonApiResponse<TAttrs, IsArray> ): IsArray extends true ? Resource<TAttrs>[] : Resource<TAttrs> { if (Array.isArray(response.data)) { return response.data.map(item => ({ id: item.id, type: item.type, ...item.attributes })) as ReturnType<typeof parseData<TAttrs, IsArray>>; } else { return { id: response.data.id, type: response.data.type, ...item.attributes } as ReturnType<typeof parseData<TAttrs, IsArray>>; } }
使用示例
// 显式指定返回单个对象(第二个泛型参数传false) const singleResult = parseData<{ name: string; price: number }, false>(singleArticle); // 显式指定返回数组(第二个泛型参数传true) const arrayResult = parseData<{ name: string; price: number }, true>(articleList);
方案3:用类型守卫细化返回类型
如果无法提前确定输入的响应类型,可以用自定义类型守卫来判断,在代码分支里明确结果类型:
interface JsonApiResource<TAttrs> { type: string; id: string; attributes: TAttrs; } type Resource<TAttrs> = { id: string; type: string } & TAttrs; // 自定义类型守卫:判断响应的data是否为数组 function isResourceArray<T>( response: { data: JsonApiResource<T> | JsonApiResource<T>[] } ): response is { data: JsonApiResource<T>[] } { return Array.isArray(response.data); } function parseData<T>( response: { data: JsonApiResource<T> | JsonApiResource<T>[] } ): Resource<T> | Resource<T>[] { if (isResourceArray(response)) { return response.data.map(item => ({ ...item, ...item.attributes })); } else { return { ...response.data, ...response.data.attributes }; } }
使用示例
// 假设response是动态获取的,类型不确定 const response: { data: JsonApiResource<{name: string; price: number}> | JsonApiResource<{name: string; price: number}>[] } = Math.random() > 0.5 ? singleArticle.data : articleList.data; if (isResourceArray({ data: response })) { // 这里result被推断为Resource数组类型 const result = parseData({ data: response }) as Resource<{name: string; price: number}>[]; } else { // 这里result被推断为单个Resource类型 const result = parseData({ data: response }) as Resource<{name: string; price: number}>; }
选哪个方案?
- 优先用函数重载:最省心,TypeScript自动推导类型,代码也简洁。
- 需要显式控制返回类型时用泛型参数标记:灵活性更高,适合需要明确指定返回形态的场景。
- 动态场景用类型守卫:适合无法提前确定输入类型的情况,但需要额外的类型判断代码。
内容的提问来源于stack exchange,提问作者christophedeloor
相关产品推荐
相关产品推荐

