Typed contracts
@tevm/ethers exports Contract and Interface. They are the same runtime values as
ethers.Contract and ethers.Interface — only the types are narrowed — so adopting them is a
one-line import change with no runtime cost and no code generation step.
- import { Contract, Interface } from 'ethers'
+ import { Contract, Interface } from '@tevm/ethers'The as const requirement
Type inference comes from the ABI's literal type. Without as const (or an ABI imported from a
.ts file that already has it), TypeScript widens 'function' to string and you fall back to the
untyped ethers behavior.
import { , } from '@tevm/ethers'
const = [
{
: 'function',
: 'balanceOf',
: 'view',
: [{ : 'account', : 'address' }],
: [{ : 'balance', : 'uint256' }],
},
{
: 'function',
: 'transfer',
: 'nonpayable',
: [
{ : 'to', : 'address' },
{ : 'amount', : 'uint256' },
],
: [{ : '', : 'bool' }],
},
{
: 'event',
: 'Transfer',
: [
{ : true, : 'from', : 'address' },
{ : true, : 'to', : 'address' },
{ : false, : 'value', : 'uint256' },
],
},
] as
const = await .createMemoryProvider({})
const = new ('0x0000000000000000000000000000000000000001', , )
// `balance` is `bigint`; `token.balnceOf` is a compile error
const = await .balanceOf('0x0000000000000000000000000000000000000002')Read vs write methods
The type of a method's return value follows its state mutability:
pure/view→ the decoded return value (bigint,string,boolean, ...). One output is unwrapped; multiple outputs become an ethersResultintersected with the output tuple.nonpayable/payable→ aContractTransactionResponse. Use.staticCall()to get the decoded return value instead, and.estimateGas()/.populateTransaction()as usual.
A complete, runnable example
This deploys the ERC20 that ships with @tevm/contract, funds an account with deal, transfers
tokens with an ethers Wallet, and reads the emitted event back — entirely in memory.
import { , } from '@tevm/ethers'
import { } from '@tevm/contract'
import { , } from '@tevm/utils'
import { } from 'ethers'
const = await .createMemoryProvider({
: { : 'auto' },
})
const = await .tevm.deploy({
....('Token', 'TKN'),
: .,
: .,
: [0].,
: true,
})
const = new ([0], )
const = new (.createdAddress, ., )
.(await .name()) // 'Token'
.(await .symbol()) // 'TKN'
.(await .decimals()) // 18n
// Give the sender a balance without needing a mint function
await .tevm.deal({
: .createdAddress,
: [0].,
: 1000n,
})
.(await .balanceOf([0].)) // 1000n
const = await .transfer(`0x${'42'.(20)}`, 100n)
const = await .wait()
.(?.status) // 1
.(await .balanceOf(`0x${'42'.(20)}`)) // 100n
const = await .queryFilter('Transfer', 0, 'latest')
.(.length) // 1
.([0].args.value) // 100n
.([0].args.to) // '0x4242424242424242424242424242424242424242'queryFilter narrows args for every event name in the ABI. Unknown event names still work and
fall back to EventLog | Log.
Interface
Interface keeps the const ABI in its fragments property, which is useful when you encode or
decode calldata by hand.
import { } from '@tevm/ethers'
const = [
{
: 'function',
: 'balanceOf',
: 'view',
: [{ : 'account', : 'address' }],
: [{ : 'balance', : 'uint256' }],
},
] as
const = new <typeof >()
const = .encodeFunctionData('balanceOf', ['0x0000000000000000000000000000000000000001'])
.(.startsWith('0x70a08231')) // trueWorking with Tevm contract objects
createContract from @tevm/contract builds an ABI from human-readable signatures, and its .abi
is already const, so it plugs straight into the typed Contract.
import { , } from '@tevm/ethers'
import { } from '@tevm/contract'
const = ({
: 'Counter',
: [
'function increment() public',
'function count() public view returns (uint256)',
],
})
const = await .createMemoryProvider({})
const = new ('0x0000000000000000000000000000000000000001', ., )Limitations
- ethers v6 only. There is no v5 build.
- Overloaded functions resolve to the ethers runtime behavior; disambiguate with the full signature
(
contract['transfer(address,uint256)']) when you need it. - Structs (
tuple) decode to an ethersResultintersected with the tuple type, so both index and name access type-check.

