If you're still hand-rolling ethers.Contract types, you're losing free wins. viem's ABI inference catches whole classes of bugs before they ship.
The problem with stringly-typed ABIs
const c = new ethers.Contract(addr, abi, provider);
const x = await c.balanceOf(wallet); // anyx is any. The compiler can't help when you typo balanceof, swap argument order, or change a return tuple. You find out at runtime, often in production.
viem's approach
import { getContract } from "viem";
const erc20 = getContract({
address: addr,
abi: erc20Abi, // ABI as a `const`-asserted tuple
client: publicClient,
});
const balance = await erc20.read.balanceOf([wallet]);
// ^? bigint — inferred from the ABIThe as const on the ABI is doing all the work. viem walks the literal type and reconstructs argument tuples, return shapes, event payloads — every function call is fully typed end-to-end.
Why this matters in practice
- Renames break the build, not the app. Rename a function in your Solidity contract, regenerate the ABI, and TypeScript will point at every callsite.
- Event payloads aren't
any. Log decoders get the same treatment —args.from,args.to,args.valueare properly typed. - No more
as. Casts are a smell. With viem, you almost never need one.
One catch
Make sure your ABI export is as const:
export const erc20Abi = [
// ...
] as const;If you import from a tool that doesn't preserve literal types (looking at you, certain auto-generators), wrap it in a typed re-export.
Bottom line
For new code in 2026, default to viem + wagmi. The DX gap on ethers v6 is too wide to justify the migration cost on greenfield projects.