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

TypeScript:如何基于Endpoint的decoder属性确定call函数返回类型?

类型安全API包装器中call()函数的类型错误问题解决

我尝试编写一个类型安全的API包装器,核心是call()函数,它接收一个endpoint对象,该对象描述目标端点、请求体类型(若存在)和预期响应体类型(若存在)。当endpoint.decoder(类型为Decoder<ResType>)存在时,call()应返回Promise<ResType>;否则返回Promise<void>。

给定类型定义

type Encoder<MsgType extends object> = {
  encode(msg: MsgType): Uint8Array;
};

type Decoder<MsgType extends object> = {
  decode(bin: Uint8Array): MsgType;
};

type Endpoint<ReqType extends object | void = void, ResType extends object | void = void> = {
  path: string;
  decoder: ResType extends object ? Decoder<ResType> : never;
} & (
  | {
      method: 'PUT' | 'POST';
      encoder: ReqType extends object ? Encoder<ReqType> : never;
    }
  | { method: 'GET'; encoder: never }
);

encoder和decoder属性的存在决定了端点是否需要请求体或响应体(GET请求明确不允许请求体)。

编写的call()函数及错误

async function call<ReqType extends object | void, ResType extends object | void>(
  endpoint: Endpoint<ReqType, ResType>,
  message?: ReqType
): Promise<ResType> {
    let body: BodyInit | undefined;
    if (endpoint.encoder && message) {
      body = endpoint.encoder.encode(message);  // 此处无问题
    }
    
    const response = await fetch('/api' + endpoint.path, { method: endpoint.method, body })

    if (endpoint.decoder) {
      const data = await response.arrayBuffer();
      /* 错误:Type 'object' is not assignable to type 'ResType'.
           'object' is assignable to the constraint of type 'ResType', but 'ResType' could be instantiated with a different subtype of constraint 'void | object'. */
      return endpoint.decoder.decode(new Uint8Array(data));
    } else {
      /* 错误:Type 'void' is not assignable to type 'ResType'.
           'void' is assignable to the constraint of type 'ResType', but 'ResType' could be instantiated with a different subtype of constraint 'void | object'. */
      return Promise.resolve();
    }
}

疑问

既然endpoint.decoder的类型是ResType extends object ? Decoder<ResType> : never,那检查endpoint.decoder是否存在,难道不应该断言ResType确实是object类型(而非void)吗?为什么还会出现类型错误?

解决方案

问题根源在于TypeScript的控制流分析无法直接将endpoint.decoder的存在性与泛型参数ResType的类型分支关联起来。虽然decoder的类型依赖于ResType是否为object,但TypeScript不能自动推导这个关联,需要我们手动细化类型。

方案1:使用类型断言明确分支类型

在两个返回分支中,分别用类型断言明确返回值的类型,匹配ResType的实际分支:

async function call<ReqType extends object | void, ResType extends object | void>(
  endpoint: Endpoint<ReqType, ResType>,
  message?: ReqType
): Promise<ResType> {
    let body: BodyInit | undefined;
    if (endpoint.encoder && message) {
      body = endpoint.encoder.encode(message);
    }
    
    const response = await fetch('/api' + endpoint.path, { method: endpoint.method, body })

    if (endpoint.decoder) {
      const data = await response.arrayBuffer();
      // 此时ResType必然是object类型,断言为ResType
      return endpoint.decoder.decode(new Uint8Array(data)) as ResType;
    } else {
      // 此时ResType必然是void类型,转换后断言为Promise<ResType>
      return Promise.resolve() as unknown as Promise<ResType>;
    }
}

方案2:自定义类型守卫细化泛型

定义一个自定义类型守卫,帮助TypeScript识别decoder存在时ResType的类型:

// 类型守卫:判断endpoint是否带有decoder,此时ResType必为非void的object类型
function hasDecoder<ResType extends object | void>(
  endpoint: Endpoint<any, ResType>
): endpoint is Endpoint<any, Exclude<ResType, void>> {
  return !!endpoint.decoder;
}

async function call<ReqType extends object | void, ResType extends object | void>(
  endpoint: Endpoint<ReqType, ResType>,
  message?: ReqType
): Promise<ResType> {
    let body: BodyInit | undefined;
    if (endpoint.encoder && message) {
      body = endpoint.encoder.encode(message);
    }
    
    const response = await fetch('/api' + endpoint.path, { method: endpoint.method, body })

    if (hasDecoder(endpoint)) {
      const data = await response.arrayBuffer();
      // TypeScript已推断出ResType是object类型,无需额外断言
      return endpoint.decoder.decode(new Uint8Array(data));
    } else {
      // TypeScript已推断出ResType是void类型
      return Promise.resolve() as Promise<ResType>;
    }
}

方案3:重构Endpoint为明确联合类型

将Endpoint拆分为更清晰的联合类型,让TypeScript的控制流分析更容易识别类型分支:

type BaseEndpoint = { path: string };

// GET请求端点:无encoder,decoder可选(依赖ResType是否为object)
type GetEndpoint<ResType extends object | void = void> = BaseEndpoint & {
  method: 'GET';
  encoder: never;
  decoder: ResType extends object ? Decoder<ResType> : never;
};

// PUT/POST请求端点:必有encoder,decoder可选(依赖ResType是否为object)
type MutateEndpoint<ReqType extends object, ResType extends object | void = void> = BaseEndpoint & {
  method: 'PUT' | 'POST';
  encoder: Encoder<ReqType>;
  decoder: ResType extends object ? Decoder<ResType> : never;
};

// 重构后的Endpoint联合类型
type Endpoint<ReqType extends object | void = void, ResType extends object | void = void> = 
  ReqType extends void ? GetEndpoint<ResType> : MutateEndpoint<ReqType, ResType>;

重构后,call()函数无需额外断言即可通过类型检查,TypeScript能自动识别不同分支的返回类型。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 00:22:34