基于字面量类型定义Partial Record键,解决Dex交换方法类型报错
问题背景
我需要给所有已有的Dex抽象类实现准确的swap方法,目前写了如下TypeScript代码:
// 这是UniswapV2Router抽象类各swap方法处理器的联合输出类型 // 不同DEX的swap处理器返回值可能不同,未来会扩展其他DEX的对应类型 type FinderOutput = UniswapV2MethodOutput; // 这个类型定义了抽象类中带swap处理器的方法名,目前比较通用 // 未来想让它更具体化,能通过方法名识别swap方法类型 type ContractMethodImplementations = Record<string, (...args: any[]) => Promise<FinderOutput>>; // 这个类型列出了所有要实现的DEX名称 type ContractNames = "UniswapV2" | "UniswapV3" | ...; // 这个映射表关联DEX名称到对应的swap方法处理器,比如UniswapV2Router.swapExactETHForTokensHandler const contractMethodsImplementations: Partial<Record<ContractNames, ContractMethodImplementations>> = { UniswapV2: { swapExactETHForTokensHandler: UniswapV2Router.swapExactETHForTokensHandler, // 其他方法 }, // 未来需要的其他DEX };
因为还没完成所有实现,用了Partial<Record>,但即使通过if (contractMethodsImplementations[contractType])判断存在,TypeScript还是提示contractMethodsImplementations[contractType]可能为undefined,没法安全访问里面的方法。我想实现一个methodFinder函数,根据合约类型和方法名获取对应的swap方法处理函数,求更优的实现方案或解决办法:
export function methodFinder( tx: TransactionResponse, contractType: ContractNames, abi: InterfaceAbi, decodedData: TransactionDescription, ): FinderOutput { const { name: methodName, args: methodArgs, value: inputValue } = decodedData; if (contractMethodsImplementations[contractType]) { contractMethodsImplementations[contractType][methodName]; // 这里要把参数传给对应的处理器并执行,返回结果 } }
解决方案
方案1:自定义类型守卫缩小类型范围
写一个自定义类型守卫,明确告知TypeScript当前contractType对应的方法映射是存在的,消除undefined警告:
function hasContractMethods(contractType: ContractNames): contractType is keyof typeof contractMethodsImplementations { return !!contractMethodsImplementations[contractType]; } export async function methodFinder( tx: TransactionResponse, contractType: ContractNames, abi: InterfaceAbi, decodedData: TransactionDescription, ): Promise<FinderOutput> { const { name: methodName, args: methodArgs, value: inputValue } = decodedData; if (hasContractMethods(contractType)) { const methods = contractMethodsImplementations[contractType]; const handler = methods[methodName]; if (!handler) { throw new Error(`未找到合约 ${contractType} 中方法 ${methodName} 的处理器`); } // 根据实际处理器的参数要求传递参数 return handler(tx, methodArgs, inputValue, abi); } throw new Error(`合约类型 ${contractType} 暂未实现`); }
方案2:非空断言(谨慎使用)
如果能保证传入的contractType一定是已经实现的DEX类型,可以用非空断言!强制告诉TypeScript该值不为undefined:
export async function methodFinder( tx: TransactionResponse, contractType: ContractNames, abi: InterfaceAbi, decodedData: TransactionDescription, ): Promise<FinderOutput> { const { name: methodName, args: methodArgs, value: inputValue } = decodedData; const methods = contractMethodsImplementations[contractType]!; const handler = methods[methodName]; if (!handler) { throw new Error(`未找到合约 ${contractType} 中方法 ${methodName} 的处理器`); } return handler(tx, methodArgs, inputValue, abi); }
注意:该方案风险较高,若后续新增ContractNames但未实现对应方法映射,会触发运行时错误。
方案3:拆分类型,区分已实现与待实现DEX
将ContractNames拆分为已实现和待实现两个类型,让方法映射表只对应已实现的DEX,彻底避免Partial带来的undefined问题:
// 已实现的DEX类型 type ImplementedContractNames = "UniswapV2"; // 所有需要实现的DEX类型 type ContractNames = ImplementedContractNames | "UniswapV3" | ...; // 无需Partial,只映射已实现的DEX const contractMethodsImplementations: Record<ImplementedContractNames, ContractMethodImplementations> = { UniswapV2: { swapExactETHForTokensHandler: UniswapV2Router.swapExactETHForTokensHandler, // 其他方法 }, }; export async function methodFinder( tx: TransactionResponse, contractType: ContractNames, abi: InterfaceAbi, decodedData: TransactionDescription, ): Promise<FinderOutput> { const { name: methodName, args: methodArgs, value: inputValue } = decodedData; if (contractType in contractMethodsImplementations) { const methods = contractMethodsImplementations[contractType as ImplementedContractNames]; const handler = methods[methodName]; if (!handler) { throw new Error(`未找到合约 ${contractType} 中方法 ${methodName} 的处理器`); } return handler(tx, methodArgs, inputValue, abi); } throw new Error(`合约类型 ${contractType} 暂未实现`); }
这个方案更严谨,能在编译阶段就区分已实现和未实现的DEX,减少运行时错误。
补充优化:精细化方法名类型匹配
如果想进一步提升类型安全性,可以给每个DEX定义专属的方法名类型,替换通用的string,让TypeScript检查方法名的合法性:
// UniswapV2专属方法名类型 type UniswapV2MethodNames = "swapExactETHForTokensHandler" | "swapExactTokensForETHHandler"; // UniswapV3专属方法名类型 type UniswapV3MethodNames = "swapExactInputHandler" | ...; // 关联DEX类型到对应的方法名和处理器类型 type ContractMethodMap = { UniswapV2: Record<UniswapV2MethodNames, (...args: any[]) => Promise<FinderOutput>>; UniswapV3: Record<UniswapV3MethodNames, (...args: any[]) => Promise<FinderOutput>>; }; const contractMethodsImplementations: Partial<ContractMethodMap> = { UniswapV2: { swapExactETHForTokensHandler: UniswapV2Router.swapExactETHForTokensHandler, // 其他方法 }, };
这样TypeScript会自动校验方法名是否属于对应DEX的合法方法,避免拼写错误。
内容的提问来源于stack exchange,提问作者Vlad

