китаев.tech

Автовывод типов из ABI: как это работает

Включишь автовывод типов viem через ABI as const или parseAbi и увидишь, какие ошибки TypeScript ловит до запуска.

Типы из ABI, без codegen

viem выводит TypeScript-типы прямо из ABI: допустимые functionName, форма args и тип возвращаемого значения. Отдельный typechain / ручные интерфейсы для простых сценариев не нужны — достаточно as const или parseAbi.

Без литерального ABI TypeScript видит «просто массив объектов» и не знает ни имён функций, ни типов аргументов.


ABI с as const и типизированный read

typescript
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 — он тоже сохраняет литеральные типы:

typescript
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 и посмотри ошибки:

typescript
// 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}`, uint256bigint, uint8number, boolboolean, stringstring.

functionName становится union имён из ABI. Выбранное имя сужает args и тип результата — это conditional types, не runtime-магия.

Если аннотировать вручную const abi: Abi = [...], литералы стираются и инференс умирает. Либо as const, либо parseAbi, либо satisfies аккуратно — но не широкая аннотация Abi на том же объекте, из которого ждёшь автокомплит.


Если ты знаешь ethers.js

ethers v6viem
типы контракта слабые / через TypeChainинференс из ABI as const / parseAbi
contract.balanceOf(addr) — args почти anyневерный тип аргумента = ошибка компиляции
codegen (typechain) для строгостиcodegen не обязателен для типовой безопасности вызовов
human-readable ABI в runtimeparseAbi([...]) — и runtime, и типы

Частые ошибки

Забыли as const → код в runtime работает, автокомплит и проверка args — нет. Это самая частая причина «у меня не работает типизация viem».

const abi: Abi = [...] as const → аннотация Abi снова расширяет тип. Пиши const abi = [...] as const без широкой аннотации.

Путают runtime-ошибку RPC и ошибку TypeScript → неверный functionName в строке без as const пройдёт компиляцию и упадёт уже в сети. С инференсом такая опечатка видна в IDE.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.