Walkthrough
Let's use ABIType to create a type-safe function that calls "read" contract methods. We'll infer function names, argument types, and return types from a user-provided ABI, and make sure it works for function overloads.
You can spin up a TypeScript Playground to code along.
1. Scaffolding readContract
First, we start off by declaring1 the function readContract
with some basic types:
import { type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi } from 'abitype'
declare function function readContract(config: {
abi: Abi;
functionName: string;
args: readonly unknown[];
}): unknownreadContract(config: {
abi: Abi;
functionName: string;
args: readonly unknown[];
}config: {
abi: Abiabi: type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi
functionName: stringfunctionName: string
args: readonly unknown[]args: readonly unknown[]
}): unknown
The function accepts a config
object which includes the ABI, function name, and arguments. The return type is unknown
since we don't know what the function will return quite yet.2 Next, let's call the function using the following values:
import { const abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi } from './abi'
const const res: unknownres = function readContract(config: {
abi: Abi;
functionName: string;
args: readonly unknown[];
}): unknownreadContract({
abi: Abiabi,
functionName: stringfunctionName: 'balanceOf',
args: readonly unknown[]args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'],
})
2. Adding inference to functionName
functionName
and args
types aren't inferred from the ABI yet so we can pass any value we want. Let's fix that! Often, you'll want to pull types into generics when trying to infer parameters. We'll do the same here, starting with functionName
:
import { type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi, type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames } from 'abitype'
import { const abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi } from './abi'
declare function function readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: TFunctionName | ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>;
args: readonly unknown[];
}): unknownreadContract<
function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: readonly unknown[];
}): unknownTAbi extends type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi,
function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: readonly unknown[];
}): unknownTFunctionName extends type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: readonly unknown[];
}): unknownTAbi, 'pure' | 'view'>,
>(config: {
abi: TAbi;
functionName: TFunctionName | ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>;
args: readonly unknown[];
}config: {
abi: TAbi extends Abiabi: function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: readonly unknown[];
}): unknownTAbi
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionNamefunctionName: function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: readonly unknown[];
}): unknownTFunctionName | type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: readonly unknown[];
}): unknownTAbi, 'pure' | 'view'>
args: readonly unknown[]args: readonly unknown[]
}): unknown
const const res: unknownres = function readContract<readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}], "balanceOf">(config: {
...;
}): unknownreadContract({
abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi,
functionName: "balanceOf" | "tokenURI"functionName: 'balanceOf',
args: readonly unknown[]args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'],
})
First, we create two generics TAbi
and TFunctionName
, and constrain their types. TAbi
is set to the config.abi
property and the Abi
type. For TFunctionName
, we import ExtractAbiFunctionNames
and use it to parse out all the read function names (state mutability 'pure' | 'view'
3) from the ABI. Finally, config.functionName
is set to the user-defined TFunctionName
and another instance of ExtractAbiFunctionNames
. This allows us to add the full union (not just the current value) to functionName
's scope.4
If you are following along in a TypeScript Playground or editor, you can try various values for functionName
. functionName
will autocomplete and only accept 'balanceOf' | 'tokenURI'
. You can also try renaming the function names in abi
and types will update as well.
const const res: unknownres = function readContract<readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}], "balanceOf" | "tokenURI">(config: {
...;
}): unknownreadContract({
abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi,
functionName: "balanceOf" | "tokenURI"functionName: '- 'balanceOf'
- 'tokenURI'
})
3. Adding inference to args
With functionName
complete, we can move on to args
. This time we don't need to add a generic slot because args
depends completely on abi
and functionName
and doesn't need to infer user input.
import {
type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi,
type AbiParametersToPrimitiveTypes<TAbiParameters extends readonly AbiParameter[], TAbiParameterKind extends AbiParameterKind = AbiParameterKind> = { [K in keyof { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }]: { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }[K]; }Converts array of AbiParameter to corresponding TypeScript primitive types.
AbiParametersToPrimitiveTypes,
type ExtractAbiFunction<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, AbiStateMutability>, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> extends {
name: TFunctionName;
} ? {
name: TFunctionName;
} & Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> : neverExtracts AbiFunction with name from Abi.
ExtractAbiFunction,
type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames,
} from 'abitype'
import { const abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi } from './abi'
declare function function readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: TFunctionName | ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>;
args: AbiParametersToPrimitiveTypes<ExtractAbiFunction<TAbi, TFunctionName>['inputs'], 'inputs'>;
}): unknownreadContract<
function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTAbi extends type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi,
function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTFunctionName extends type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTAbi, 'pure' | 'view'>,
>(config: {
abi: TAbi;
functionName: TFunctionName | ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>;
args: AbiParametersToPrimitiveTypes<ExtractAbiFunction<TAbi, TFunctionName>['inputs'], 'inputs'>;
}config: {
abi: TAbi extends Abiabi: function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTAbi
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionNamefunctionName: function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTFunctionName | type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTAbi, 'pure' | 'view'>
args: { [K in keyof { [K in keyof ExtractAbiFunction<TAbi, TFunctionName>["inputs"]]: AbiParameterToPrimitiveType<ExtractAbiFunction<TAbi, TFunctionName>["inputs"][K], "inputs">; }]: { [K in keyof ExtractAbiFunction<TAbi, TFunctionName>["inputs"]]: AbiParameterToPrimitiveType<ExtractAbiFunction<TAbi, TFunctionName>["inputs"][K], "inputs">; }[K]; }args: type AbiParametersToPrimitiveTypes<TAbiParameters extends readonly AbiParameter[], TAbiParameterKind extends AbiParameterKind = AbiParameterKind> = { [K in keyof { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }]: { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }[K]; }Converts array of AbiParameter to corresponding TypeScript primitive types.
AbiParametersToPrimitiveTypes<
type ExtractAbiFunction<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, AbiStateMutability>, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> extends {
name: TFunctionName;
} ? {
name: TFunctionName;
} & Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> : neverExtracts AbiFunction with name from Abi.
ExtractAbiFunction<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTAbi, function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">>(config: {
abi: TAbi;
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionName;
args: { [K in keyof { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof ExtractAbiFunction<...>["inputs"]]: AbiParameterToPrimitiveType<...>; }[K]; };
}): unknownTFunctionName>['inputs'],
'inputs'
>
}): unknown
const const res: unknownres = function readContract<readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}], "balanceOf">(config: {
...;
}): unknownreadContract({
abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi,
functionName: "balanceOf" | "tokenURI"functionName: 'balanceOf',
args: readonly [`0x${string}`] | readonly [`0x${string}`, bigint]args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'],
})
Since args
's type can be completely defined inline, we import ExtractAbiFunction
and AbiParametersToPrimitiveTypes
and wire them up. First, we use ExtractAbiFunction
to get the function from the ABI that matches TFunctionName
. Then, we use AbiParametersToPrimitiveTypes
to convert the function's inputs to their TypeScript primitive types.
For abi
, you'll notice there are two 'balanceOf'
functions. This means 'balanceOf'
is overloaded on the contract. The cool thing about TypeScript is that we can still infer the correct types for overloaded functions (e.g. union like readonly [`0x${string}`] | readonly [`0x${string}`, bigint]
)! This uses a TypeScript feature called distributivity and is worth learning more about if you're interested.
4. Adding the return type
Finally, we can add the return type:
import {
type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi,
type AbiFunction = {
type: 'function';
constant?: boolean | undefined;
gas?: number | undefined;
inputs: readonly AbiParameter[];
name: string;
outputs: readonly AbiParameter[];
payable?: boolean | undefined;
stateMutability: AbiStateMutability;
}ABI "function" type
AbiFunction,
type AbiParametersToPrimitiveTypes<TAbiParameters extends readonly AbiParameter[], TAbiParameterKind extends AbiParameterKind = AbiParameterKind> = { [K in keyof { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }]: { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }[K]; }Converts array of AbiParameter to corresponding TypeScript primitive types.
AbiParametersToPrimitiveTypes,
type ExtractAbiFunction<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, AbiStateMutability>, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> extends {
name: TFunctionName;
} ? {
name: TFunctionName;
} & Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> : neverExtracts AbiFunction with name from Abi.
ExtractAbiFunction,
type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames,
} from 'abitype'
import { const abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi } from './abi'
declare function function readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
abi: TAbi;
functionName: TFunctionName | ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>;
args: AbiParametersToPrimitiveTypes<TAbiFunction['inputs'], 'inputs'>;
}): AbiParametersToPrimitiveTypes<TAbiFunction['outputs'], 'outputs'>readContract<
function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbi extends type Abi = readonly (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[]Contract ABI Specification
Abi,
function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TFunctionName extends type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbi, 'pure' | 'view'>,
function (type parameter) TAbiFunction in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbiFunction extends type AbiFunction = {
type: 'function';
constant?: boolean | undefined;
gas?: number | undefined;
inputs: readonly AbiParameter[];
name: string;
outputs: readonly AbiParameter[];
payable?: boolean | undefined;
stateMutability: AbiStateMutability;
}ABI "function" type
AbiFunction = type ExtractAbiFunction<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, AbiStateMutability>, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> extends {
name: TFunctionName;
} ? {
name: TFunctionName;
} & Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}> : neverExtracts AbiFunction with name from Abi.
ExtractAbiFunction<
function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbi,
function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TFunctionName
>,
>(config: {
abi: TAbi;
functionName: TFunctionName | ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>;
args: AbiParametersToPrimitiveTypes<TAbiFunction['inputs'], 'inputs'>;
}config: {
abi: TAbi extends Abiabi: function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbi
functionName: ExtractAbiFunctionNames<TAbi, "view" | "pure"> | TFunctionNamefunctionName: function (type parameter) TFunctionName in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TFunctionName | type ExtractAbiFunctionNames<TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability> = Extract<TAbi[number], {
type: "function";
stateMutability: TAbiStateMutability;
}>["name"]Extracts all AbiFunction names from Abi.
ExtractAbiFunctionNames<function (type parameter) TAbi in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbi, 'pure' | 'view'>
args: { [K in keyof { [K in keyof TAbiFunction["inputs"]]: AbiParameterToPrimitiveType<TAbiFunction["inputs"][K], "inputs">; }]: { [K in keyof TAbiFunction["inputs"]]: AbiParameterToPrimitiveType<TAbiFunction["inputs"][K], "inputs">; }[K]; }args: type AbiParametersToPrimitiveTypes<TAbiParameters extends readonly AbiParameter[], TAbiParameterKind extends AbiParameterKind = AbiParameterKind> = { [K in keyof { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }]: { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }[K]; }Converts array of AbiParameter to corresponding TypeScript primitive types.
AbiParametersToPrimitiveTypes<function (type parameter) TAbiFunction in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbiFunction['inputs'], 'inputs'>
}): type AbiParametersToPrimitiveTypes<TAbiParameters extends readonly AbiParameter[], TAbiParameterKind extends AbiParameterKind = AbiParameterKind> = { [K in keyof { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }]: { [K in keyof TAbiParameters]: AbiParameterToPrimitiveType<TAbiParameters[K], TAbiParameterKind>; }[K]; }Converts array of AbiParameter to corresponding TypeScript primitive types.
AbiParametersToPrimitiveTypes<function (type parameter) TAbiFunction in readContract<TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames<TAbi, "view" | "pure">, TAbiFunction extends AbiFunction = ExtractAbiFunction<TAbi, TFunctionName>>(config: {
...;
}): { [K in keyof { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }]: { [K in keyof TAbiFunction["outputs"]]: AbiParameterToPrimitiveType<...>; }[K]; }TAbiFunction['outputs'], 'outputs'>
const const res: readonly [bigint]res = function readContract<readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}], "balanceOf", {
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
} | {
...;
}>(config: {
...;
}): readonly [...]readContract({
abi: readonly [{
readonly name: "balanceOf";
readonly type: "function";
readonly stateMutability: "view";
readonly inputs: readonly [{
readonly name: "owner";
readonly type: "address";
}];
readonly outputs: readonly [{
readonly name: "balance";
readonly type: "uint256";
}];
}, {
...;
}, {
...;
}, {
...;
}]abi,
functionName: "balanceOf" | "tokenURI"functionName: 'balanceOf',
args: readonly [`0x${string}`] | readonly [`0x${string}`, bigint]args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'],
})
We can refactor our ExtractAbiFunction
call into a generic slot TAbiFunction
(of type AbiFunction
) and set the default to the result of ExtractAbiFunction
. This allows us to use TAbiFunction
in for args
and the return type. Lastly, we wire up another AbiParametersToPrimitiveTypes
call for the return type—this time using outputs.
5. Wrapping up
readContract
's types are starting to look solid! It infers the correct types for functionName
and args
based on the ABI (and works with overloaded functions). It also infers the correct return type based on the ABI and functionName
. The only thing left is to implement the function itself.
There are a few other ways to improve the typing that are out of scope for this walkthrough, but are worth noting:
abi
requires a const assertion to ensure TypeScript takes the most specific type, but you can set things up so this is unnecessary for inlineabi
definitions.args
can be an empty array if the function doesn't take any arguments, but you could conditionally addargs
toconfig
if it's not empty.readContract
's return type is an array, but you could unwrap it if the function only has one output or transform it to another type depending on your implementation.
The preceding points are all implemented in throughout the examples in this directory so check them out if you're interested.
Footnotes
-
We use the
declare
keyword so we don't need to worry about the implementation. In this case, the implementation would look something like encoding arguments and sending with theeth_call
RPC method. ↩ -
If this was a real function that read via RPC, we'd likely want to make it
async
and return aPromise
, but we'll leave that out for simplicity. ↩ -
We could add or change this to
'nonpayable' | 'payable'
to allow write functions. ↩ -
Try removing
| ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>
fromfunctionName
, hover overfunctionName
in your editor, and see what happens. You'll notice that the onlyfunctionName
that shows up in the current value. ↩