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

复杂泛型异步返回类型无法推断具体返回值问题求助

重构TypeScript通用API处理器以实现精确类型推断

核心问题分析

原代码中泛型参数未与处理器的输入输出强绑定,导致错误码被宽泛推断为string,成功数据被推断为通用的Jsonifiable而非具体结构。本质是泛型推导链条断裂,无法从业务逻辑中捕获细化的类型信息。

重构方案

1. 定义判别式联合响应类型

先为成功/错误响应定义带类型标记的严格结构,让TypeScript能自动分支细化:

// 错误响应:绑定具体错误码字面量
type ApiError<Code extends string> = {
  type: "error";
  code: Code;
  message?: string;
};

// 成功响应:绑定具体数据结构
type ApiSuccess<Data> = {
  type: "success";
  data: Data;
};

// 统一API响应类型:通过type字段做判别式
type ApiResponse<Data, ErrorCode extends string> = 
  | ApiSuccess<Data> 
  | ApiError<ErrorCode>;

2. 让处理器与业务配置强绑定

将API的业务逻辑、错误映射作为泛型参数的推导源,确保TypeScript能从配置中捕获精确类型:

// API处理器配置类型
type ApiHandlerConfig<Data, ErrorCode extends string> = {
  // 业务处理函数,返回具体成功数据或抛出异常
  handler: () => Promise<Data>;
  // 异常信息到错误码的映射,必须用as const断言锁定字面量
  errorMap: Readonly<Record<string, ErrorCode>>;
};

// 通用API处理器
async function handleApi<Data, ErrorCode extends string>(
  config: ApiHandlerConfig<Data, ErrorCode>
): Promise<ApiResponse<Data, ErrorCode>> {
  try {
    const data = await config.handler();
    return { type: "success", data };
  } catch (err) {
    const errorKey = err instanceof Error ? err.message : String(err);
    const code = config.errorMap[errorKey] ?? ("unknown" as ErrorCode);
    return { type: "error", code };
  }
}

3. 调用时自动推断精确类型

传入配置时,通过as const锁定错误码字面量,让TypeScript自动推导出细化的响应类型:

// 示例API:数字校验
const result = await handleApi({
  handler: async () => {
    const num = Math.random() * 100;
    if (num > 80) throw new Error("too big");
    if (num < 20) throw new Error("too small");
    return { foo: "bar" }; // 成功数据的具体结构
  },
  errorMap: {
    "too big": "too big",
    "too small": "too small"
  } as const // 关键:强制推断为字面量类型而非string
});

// 此时result的类型为:
// { type: "success"; data: { foo: string; }; } 
// | { type: "error"; code: "too big" | "too small"; }

// 穷尽处理所有场景(TypeScript会强制检查未覆盖分支)
switch (result.type) {
  case "success":
    console.log(result.data.foo); // 类型安全,可直接访问foo
    break;
  case "error":
    switch (result.code) {
      case "too big":
        console.log("数值超过上限");
        break;
      case "too small":
        console.log("数值低于下限");
        break;
      // 新增错误码时,TypeScript会提示未穷尽处理
    }
}

关键优化点

  • as const字面量断言:确保错误码被推断为具体字符串,而非宽泛的string类型。
  • 判别式联合类型:通过type字段让TypeScript在分支中自动缩小类型范围,实现类型安全的穷尽检查。
  • 泛型推导绑定:处理器的泛型参数直接从handler返回值和errorMap类型推导,无需手动指定。

进阶优化:自定义异常类强化类型约束

如果希望错误码与异常强绑定,可自定义异常类省略errorMap:

class ApiException<Code extends string> extends Error {
  constructor(public code: Code, message?: string) {
    super(message);
  }
}

// 简化后的处理器
async function handleApi<Data, ErrorCode extends string>(
  handler: () => Promise<Data>
): Promise<ApiResponse<Data, ErrorCode>> {
  try {
    const data = await handler();
    return { type: "success", data };
  } catch (err) {
    if (err instanceof ApiException) {
      return { type: "error", code: err.code };
    }
    return { type: "error", code: "unknown" as ErrorCode };
  }
}

// 使用示例
const result = await handleApi(async () => {
  const num = Math.random() * 100;
  if (num > 80) throw new ApiException("too big", "数值超限");
  if (num < 20) throw new ApiException("too small", "数值过低");
  return { foo: "bar" };
});
// 错误码自动推断为"too big"|"too small"

内容的提问来源于stack exchange,提问作者osdiab

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 03:50:40