You can create a type-safe wrapper for reading smart contract methods by combining ExtractAbiFunctionNames, ExtractAbiFunction, and AbiParametersToPrimitiveTypes. This setup allows TypeScript to infer the valid function names, the required argument types (including support for overloaded functions), and the resulting return types based on the provided ABI.
Key utility functions used:
ExtractAbiFunctionNames: Filters the ABI for function names matching specific state mutability (e.g., 'pure' or 'view').ExtractAbiFunction: Retrieves the full function definition from the ABI based on the function name.AbiParametersToPrimitiveTypes: Converts ABI input/output parameters into TypeScript primitive types.
import {
Abi,
AbiFunction,
AbiParametersToPrimitiveTypes,
ExtractAbiFunction,
ExtractAbiFunctionNames,
} from 'abitype'
// Example ABI with overloaded 'balanceOf' functions
export const abi = [
{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'owner', type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
},
{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'collectionId', type: 'uint256' },
],
outputs: [{ name: 'balance', type: 'uint256' }],
},
{
name: 'tokenURI',
type: 'function',
stateMutability: 'pure',
inputs: [{ name: 'id', type: 'uint256' }],
outputs: [{ name: 'uri', type: 'string' }],
},
] as const
declare function readContract<
abi extends Abi,
functionName extends ExtractAbiFunctionNames<abi, 'pure' | 'view'>,
abiFunction extends AbiFunction = ExtractAbiFunction<abi, functionName>,
>(config: {
abi: abi
functionName: functionName | ExtractAbiFunctionNames<abi, 'pure' | 'view'>
args: AbiParametersToPrimitiveTypes<abiFunction['inputs'], 'inputs'>
}): AbiParametersToPrimitiveTypes<abiFunction['outputs'], 'outputs'>
// Usage
const res = readContract({
abi,
functionName: 'balanceOf',
args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'],
})