基于可选对象属性的TypeScript条件类型问题排查
TypeScript类型适配问题:wrap函数处理可选payload属性
问题场景
现有外部API定义如下:
interface CommonRequestParams { user: {}; } function f1(params: CommonRequestParams) {/* do something */} interface SecondRequestParams extends CommonRequestParams { payload: { id: string }; } function f2(params: SecondRequestParams) {/* do something */} interface ThirdRequestParams extends CommonRequestParams { payload?: { id: number }; } function f3(params: ThirdRequestParams) {/* do something */} const api = { f1, f2, f3, };
需要实现一个wrap函数,自动注入公共的user数据,同时让调用者只需要传入payload部分(或不传),期望调用方式如下:
const _f1 = wrap(f1); const _f2 = wrap(f2); const _f3 = wrap(f3); _f1(); // 合法 _f2({ id: "1" }); // 合法 _f3(); // 合法 _f3({ id: 1 }); // 合法 _f1("unknown"); // 非法
原实现的wrap函数在处理可选payload和无payload的情况时类型不匹配,问题代码如下:
type RequestApi = typeof api; type RequestApiMethod = RequestApi[keyof RequestApi]; type RequestApiMethodPayload<M extends RequestApiMethod, P = Parameters<M>[0], > = // 问题所在 P extends CommonRequestParams & { payload?: any } ? P["payload"] : never; function wrap<M extends RequestApiMethod, >(method: M) { return (payload: RequestApiMethodPayload<M>) => { return method({ user: {}, payload, }); }; }
解决方案
调整类型逻辑,通过条件类型和infer分别处理无payload、必填payload、可选payload三种场景,同时让返回的函数参数对应可选或必填规则:
type RequestApi = typeof api; type RequestApiMethod = RequestApi[keyof RequestApi]; // 提取payload类型,分场景处理 type ExtractPayload<P> = P extends { payload: infer T } ? T : // 有必填payload,返回对应类型 P extends { payload?: infer T } ? T | void : // 有可选payload,允许传对应类型或不传 void; // 无payload,允许直接调用 // 定义包装后的函数类型,根据payload类型决定参数是否必填 type WrappedMethod<M extends RequestApiMethod> = ExtractPayload<Parameters<M>[0]> extends void ? () => ReturnType<M> : (payload: ExtractPayload<Parameters<M>[0]>) => ReturnType<M>; function wrap<M extends RequestApiMethod>(method: M): WrappedMethod<M> { return ((payload?: any) => { // 根据payload是否存在生成对应的参数对象 const params = payload !== undefined ? { user: {}, payload } : { user: {} }; return method(params as Parameters<M>[0]); }) as WrappedMethod<M>; }
代码解释
ExtractPayload类型:
- 优先匹配带有必填
payload的参数类型,返回对应的payload类型 - 再匹配带有可选
payload的参数类型,返回payload类型或void,允许调用时不传参数 - 最后处理无
payload的情况,返回void,对应直接调用的场景
- 优先匹配带有必填
WrappedMethod类型:
- 根据
ExtractPayload的结果,动态生成包装后的函数类型:如果不需要payload,则函数无参数;否则函数接收对应类型的payload参数(可选场景下因T | void允许undefined,所以调用时可以不传)
- 根据
wrap函数实现:
- 内部逻辑处理
payload是否存在的情况,生成符合原方法要求的参数对象 - 使用类型断言确保复杂条件类型的映射能被TypeScript识别
- 内部逻辑处理
验证结果
现在调用逻辑完全符合预期:
const _f1 = wrap(f1); const _f2 = wrap(f2); const _f3 = wrap(f3); _f1(); // ✅ 合法 _f2({ id: "1" }); // ✅ 合法 _f3(); // ✅ 合法 _f3({ id: 1 }); // ✅ 合法 _f1("unknown"); // ❌ 类型错误('"unknown"'无法赋值给'void'类型参数) _f2(); // ❌ 类型错误(期望传入1个参数,实际传入0个) _f3({ id: "string" }); // ❌ 类型错误('string'无法赋值给'number'类型)
内容的提问来源于stack exchange,提问作者Tamás Katona
相关产品推荐
相关产品推荐

