How to make multicalls more type safe? #3576
Replies: 1 comment
|
Hello @cursedId0l the issue you're running into happens because TypeScript collapses the types of all objects inside the multicallContracts array into a single broad union type when you construct the array using .flatMap(). When Viem receives a broad array type like (ContractFunctionConfig | ContractFunctionConfig)[], it infers the return type as a union of all possible result types (bigint | Address). Here are two primary ways to fix this and get strict, automatic type inference for each response without resorting to as bigint or as Address type assertions. Solution 1: Use as const Tuple Mapping (Recommended) import { ContractFunctionParameters } from 'viem';
// Map each contract into a fixed 2-tuple, then flatten with .flat()
const multicallContracts = vestingSplitterContracts.flatMap((address) => [
{
address,
abi: vestingSplitterABI,
functionName: 'balanceOf',
args: [ownerAddress],
},
{
address,
abi: vestingSplitterABI,
functionName: 'TOKEN',
},
] as const); // <-- Crucial: prevents type widening
const responses = await client.multicall({
contracts: multicallContracts
});When Viem reads multicallContracts as a readonly tuple instead of ContractFunctionConfig[], TypeScript evaluates the returned array as a tuple of precise response types. Solution 2: Group Calls Per Contract (Promise.all) // Map over contracts to execute structured multicalls per item
const contractData = await Promise.all(
vestingSplitterContracts.map(async (address) => {
// Passing a fixed tuple of 2 elements allows Viem to return a fixed 2-tuple result
const [balanceResp, tokenResp] = await client.multicall({
contracts: [
{
address,
abi: vestingSplitterABI,
functionName: 'balanceOf',
args: [ownerAddress],
},
{
address,
abi: vestingSplitterABI,
functionName: 'TOKEN',
},
] as const,
});
return { address, balanceResp, tokenResp };
})
);
for (const { balanceResp, tokenResp } filter in contractData) {
if (balanceResp.status === 'success' && tokenResp.status === 'success') {
// TypeScript automatically infers:
// balanceResp.result -> bigint
// tokenResp.result -> `0x${string}`
const rawBalance = balanceResp.result;
const token = getAddress(tokenResp.result);
}
}Why this works Tuple Preservation: By applying as const to the call array or passing inline arrays, TypeScript treats the contracts as [CallBalance, CallToken, CallBalance, CallToken, ...] as a fixed tuple. Viem's conditional types can then map input tuple elements directly to output tuple types. |
Uh oh!
There was an error while loading. Please reload this page.
I find myself using this pattern all the time. The problem is that
balanceResp.resultandtokenResp.resultboth have the same type -result: bigint | 0x${string}. This makes sense considering the ABI, but is there a way I can make the types stricter such that typescript can inferbalanceResp.resultis of typebigintandtokenResp.resultis of typeAddressAll reactions