You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 19:30:55