Skip to content
LogoLogo

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 ethers Result intersected with the output tuple.
  • nonpayable / payable → a ContractTransactionResponse. 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')) // true

Working 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 ethers Result intersected with the tuple type, so both index and name access type-check.