Автовывод типов из ABI: как это работает
Включишь автовывод типов viem через ABI as const или parseAbi и увидишь, какие ошибки TypeScript ловит до запуска.
Типы из ABI, без codegen
viem выводит TypeScript-типы прямо из ABI: допустимые functionName, форма args и тип возвращаемого значения. Отдельный typechain / ручные интерфейсы для простых сценариев не нужны — достаточно as const или parseAbi.
Без литерального ABI TypeScript видит «просто массив объектов» и не знает ни имён функций, ни типов аргументов.
ABI с as const и типизированный read
import { createPublicClient, http, formatUnits } from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: http("https://ethereum.publicnode.com"),
});
const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const holder = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
// as const фиксирует литералы имён и типов — без него инференса нет
const erc20Abi = [
{
type: "function",
name: "balanceOf",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ type: "uint256" }],
},
{
type: "function",
name: "decimals",
stateMutability: "view",
inputs: [],
outputs: [{ type: "uint8" }],
},
{
type: "function",
name: "transfer",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ type: "bool" }],
},
] as const;
const rawBalance = await publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "balanceOf", // автокомплит: "balanceOf" | "decimals" | "transfer"
args: [holder], // тип: readonly [`0x${string}`]
});
// typeof rawBalance → bigint
const decimals = await publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "decimals",
// args не нужны — inputs пустой
});
// typeof decimals → number (uint8)
console.log(formatUnits(rawBalance, decimals));Наведи курсор в IDE на functionName и rawBalance — типы должны совпасть с комментариями выше.
Тот же эффект через parseAbi
Если удобнее human-readable строки (как в ethers), используй parseAbi — он тоже сохраняет литеральные типы:
import { parseAbi } from "viem";
const erc20Abi = parseAbi([
"function balanceOf(address account) view returns (uint256)",
"function decimals() view returns (uint8)",
"function transfer(address to, uint256 amount) returns (bool)",
]);Дальше тот же readContract / writeContract / watchContractEvent.
Что ловит компилятор
Эти вызовы не должны проходить проверку типов. Раскомментируй в IDE и посмотри ошибки:
// 1) Несуществующая функция
await publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
// @ts-expect-error functionName не из ABI
functionName: "balanceoff",
args: [holder],
});
// 2) Неверный тип аргумента: нужен address, передан number
await publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "balanceOf",
// @ts-expect-error args: readonly [Address]
args: [123],
});
// 3) Неверное число аргументов у transfer
await publicClient.readContract({
address: usdcAddress,
abi: erc20Abi,
functionName: "transfer",
// @ts-expect-error нужно [address, uint256]
args: [holder],
});@ts-expect-error в примере означает: «здесь TypeScript обязан ругаться». Если ошибка пропала — ABI потерял литеральность (часто забыли as const или расширили тип до Abi).
Как это работает
as const делает массив readonly и сохраняет строковые литералы ("balanceOf", "address", "uint256") вместо общего string.
viem (через abitype) мапит Solidity-типы на TypeScript: address → `0x${string}`, uint256 → bigint, uint8 → number, bool → boolean, string → string.
functionName становится union имён из ABI. Выбранное имя сужает args и тип результата — это conditional types, не runtime-магия.
Если аннотировать вручную const abi: Abi = [...], литералы стираются и инференс умирает. Либо as const, либо parseAbi, либо satisfies аккуратно — но не широкая аннотация Abi на том же объекте, из которого ждёшь автокомплит.
Если ты знаешь ethers.js
| ethers v6 | viem |
|---|---|
| типы контракта слабые / через TypeChain | инференс из ABI as const / parseAbi |
contract.balanceOf(addr) — args почти any | неверный тип аргумента = ошибка компиляции |
codegen (typechain) для строгости | codegen не обязателен для типовой безопасности вызовов |
| human-readable ABI в runtime | parseAbi([...]) — и runtime, и типы |
Частые ошибки
Забыли as const → код в runtime работает, автокомплит и проверка args — нет. Это самая частая причина «у меня не работает типизация viem».
const abi: Abi = [...] as const → аннотация Abi снова расширяет тип. Пиши const abi = [...] as const без широкой аннотации.
Путают runtime-ошибку RPC и ошибку TypeScript → неверный functionName в строке без as const пройдёт компиляцию и упадёт уже в сети. С инференсом такая опечатка видна в IDE.
Что дальше
- Работа с транзакциями: nonce, gas, EIP-1559 — типизированные
sendTransactionи оценка gas - Мультивызовы: multicall — массив вызовов с теми же ABI-типами
- Чтение данных из контракта: readContract — практика view-вызовов, на которой держится инференс