Wagmi React 类型系统实战:TypeScript 要求、Register 声明合并与 const-Asserted ABI 类型推断
2026/9/17 6:00:33 网站建设 项目流程

Wagmi React 类型系统实战:TypeScript 要求、Register 声明合并与 const-Asserted ABI 类型推断

【免费下载链接】wagmiReactive primitives for Ethereum apps项目地址: https://gitcode.com/GitHub_Trending/wa/wagmi

本篇技术指南围绕 wagmi 的 TypeScript 类型体系展开:TypeScript 版本要求与strict模式配置、通过Register声明合并或 hookconfig参数跨 React Context 边界获得强类型推断、以及基于 const-asserted ABI 与 EIP-712 Typed Data 实现端到端类型安全。读完后你将掌握 wagmi 项目中的完整 TypeScript 配置方案、源码级类型推断链路(ResolvedRegisterConfigParameter、hook 泛型约束),并能落地 const assertion 让 ABI 拼写错误在编译期即被捕获。

TypeScript 版本要求与 strict 模式

wagmi 的设计目标是尽可能类型安全("as type-safe as possible")。从 packages/react/package.json 的peerDependencies可以确认当前的硬性要求:

  • typescript:>=5.9.3(且声明为optional,即仅在使用类型时需要)
  • react:>=18
  • viem:2.x(ABI 与 Typed Data 类型推断的底层引擎)
  • @tanstack/react-query:>=5.0.0

关于类型相关的版本管理,有几点必须牢记:

  1. TypeScript 不遵循 semver,minor 版本发布经常引入破坏性变更。
  2. wagmi 仓库中类型层面的变更被视为非破坏性,通常以 patch 版本发布——否则每一次类型增强都得发布一个 major 版本。
  3. 强烈建议将wagmitypescript锁定到具体的 patch 版本,并在升级时预期类型可能被修复或升级。
  4. wagmi 非类型相关的公开 API 仍然严格遵循 semver。

为确保一切正常工作,tsconfig.json中必须开启strict模式:

{ "compilerOptions": { "strict": true } }

Config 类型:跨 React Context 边界的强类型

问题背景:React Context 本身并不擅长类型推断。为了让config的类型信息穿透 Context 边界,wagmi 提供两条路径:

  • 声明合并(Declaration Merging):把config全局"注册"到 TypeScript。
  • config属性:把config直接传给 hook。

方式一:声明合并(Declaration Merging)

声明合并允许你向 TypeScript "注册"全局config。wagmi 的Register类型让框架能在原本仅靠 React Context 拿不到类型信息的地方进行推断。

@wagmi/core中,Register定义为一个空接口,等待用户通过模块增强(module augmentation)填充:

// packages/core/src/types/register.ts import type { Config } from '../createConfig.js' // biome-ignore lint/suspicious/noEmptyInterface: using export interface Register {} export type ResolvedRegister = { config: Register extends { config: infer config extends Config } ? config : Config }

从源码结构看,这段实现是理解整个机制的钥匙:

  • Register是一个空接口——空接口在 TypeScript 中天然支持声明合并,用户扩展时不会产生冲突;
  • ResolvedRegister['config']通过条件类型Register extends { config: infer config extends Config } ? config : Config做二选一解析:用户注册过就用用户注册的 config 类型,没有注册则回退到最宽泛的Config。这解释了为什么未注册时chainIdnumber而非具体联合类型。

设置方法:在项目中添加如下声明。下面的示例把声明合并与config放在一起,这也是官方脚手架模板 vite-react 模板 采用的同一模式:

import { createConfig, http } from 'wagmi' import { mainnet, sepolia } from 'wagmi/chains' declare module 'wagmi' { // [!code focus] interface Register { // [!code focus] config: typeof config // [!code focus] } // [!code focus] } // [!code focus] export const config = createConfig({ chains: [mainnet, sepolia], transports: { [mainnet.id]: http(), [sepolia.id]: http(), }, })

由于Register是全局的,整个项目只需添加一次。设置完成后,全项目获得强类型安全。以useBlockNumber为例,chainId会基于configchains进行类型推断。从 useBlockNumber.ts 的源码可以看到这条推断链:

export function useBlockNumber< config extends Config = ResolvedRegister['config'], // ← 默认值来自 Register chainId extends config['chains'][number]['id'] = config['chains'][number]['id'], // ← chainId 被约束为 config 中 chains 的 id 联合 selectData = GetBlockNumberData, >(...)

因此当你传入非法chainId时,编译器会直接报错——你在没有传入config的情况下就避免了运行时错误:

import { type Config } from 'wagmi' import { mainnet, sepolia } from 'wagmi/chains' declare module 'wagmi' { interface Register { config: Config<readonly [typeof mainnet, typeof sepolia]> } } import { useBlockNumber } from 'wagmi' useBlockNumber({ chainId: 123 }) // ❌ 编译错误:123 不在 config 的 chains 中

仓库中的类型测试 packages/register-tests/react/src/config.ts 验证了同一模式:注册包含celomainnetoptimismzkSync四条链的 config 后,ChainId类型即被推导为这四个链 id 的联合类型。

方式二:Hook 的config属性

当你拥有多个 Wagmiconfig,或不想使用声明合并时,可以直接把特定config传给 hook。config属性在类型层的定义来自 packages/core/src/types/properties.ts:

export type ConfigParameter<config extends Config = Config> = { config?: Config | config | undefined }

每个支持该参数的 hook 参数类型都会与ConfigParameter<config>做交叉。例如useReadContract的参数类型(useReadContract.ts)就是ReadContractOptions<...> & ConfigParameter<config>

运行时的解析逻辑则非常直白,见 useConfig.ts:

export function useConfig<config extends Config = ResolvedRegister['config']>( parameters: UseConfigParameters<config> = {}, ): UseConfigReturnType<config> { // 显式传入的 config 优先,否则回退到 Context const config = parameters.config ?? useContext(WagmiContext) if (!config) throw new WagmiProviderNotFoundError() return config as UseConfigReturnType<config> }

定义两个不同链的 config:

import { createConfig, http } from 'wagmi' import { mainnet, optimism } from 'wagmi/chains' export const configA = createConfig({ chains: [mainnet], transports: { [mainnet.id]: http(), }, }) export const configB = createConfig({ chains: [optimism], transports: { [optimism.id]: http(), }, })

正如预期,chainId对每个config都被正确推断:

import { type Config } from 'wagmi' import { mainnet, optimism } from 'wagmi/chains' declare const configA: Config<readonly [typeof mainnet]> declare const configB: Config<readonly [typeof optimism]> import { useBlockNumber } from 'wagmi' useBlockNumber({ chainId: 123, config: configA }) // ❌ 编译错误 useBlockNumber({ chainId: 123, config: configB }) // ❌ 编译错误

这种方式更显式,适合不使用 React Context 或声明合并的高级场景(多 config 并存、非 React 渲染层调用等)。

Const-Asserted ABI 与 Typed Data

wagmi 能够基于 ABI 与 EIP-712 Typed Data 定义推断类型——底层由 viem 与 ABIType 驱动。这带来了从合约到前端的完整端到端类型安全,以及显著的开发者体验提升:自动补全 ABI 条目名、捕获拼写错误、推断参数与返回类型(包括函数重载)等。

要让它工作,必须const-assert ABI 和 Typed Data,或者将它们内联定义。以useReadContractabi配置参数为例:

const { data } = useReadContract({ abi: […], // <--- 内联定义 })
const abi = […] as const // <--- const 断言 const { data } = useReadContract({ abi })

如果类型推断没生效,大概率是漏了const断言或没有内联定义。同时确认 ABI、Typed Data 定义以及上文提到的 TypeScript 配置(strict 模式)都正确无误。

提示:TypeScript 目前还不支持以as const导入 JSON。wagmi 仓库内置的 CLI(见 site/cli/getting-started.md)可以自动从 Etherscan 等区块浏览器拉取 ABI、从 Foundry/Hardhat 项目中解析 ABI 并生成 React Hooks,可解决这一痛点。

文档中所有出现abitypes配置属性的地方,基本都可以用 const-asserted 或内联的 ABI、Typed Data 获得类型安全与推断。

下面是 useReadContract 在 const-assert 与未 assert 两种情况下的对比。源码层面,useReadContract.ts 使用了const泛型参数(const abiconst args),保证abi字面量以只读元组形式保留,functionNameargs的约束均基于ContractFunctionName<abi, 'pure' | 'view'>ContractFunctionArgs<...>从 viem 推导:

// ✅ Const-Asserted:functionName、args 全量推断 const erc721Abi = [ { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ type: 'address', name: 'owner' }], outputs: [{ type: 'uint256' }], }, { name: 'isApprovedForAll', type: 'function', stateMutability: 'view', inputs: [ { type: 'address', name: 'owner' }, { type: 'address', name: 'operator' }, ], outputs: [{ type: 'bool' }], }, { name: 'getApproved', type: 'function', stateMutability: 'view', inputs: [{ type: 'uint256', name: 'tokenId' }], outputs: [{ type: 'address' }], }, { name: 'ownerOf', type: 'function', stateMutability: 'view', inputs: [{ type: 'uint256', name: 'tokenId' }], outputs: [{ type: 'address' }], }, { name: 'tokenURI', type: 'function', stateMutability: 'pure', inputs: [{ type: 'uint256', name: 'tokenId' }], outputs: [{ type: 'string' }], }, ] as const import { useReadContract } from 'wagmi' const { data } = useReadContract({ address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', abi: erc721Abi, functionName: 'balanceOf', // 自动补全 + 拼写检查 args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], // 参数类型来自 inputs }) // data 被推断为 uint256 对应的 bigint
// ❌ 未 const-assert:abi 退化为宽泛的 string 类型,无推断 declare const erc721Abi: { name: string type: string stateMutability: string inputs: { type: string; name: string }[] outputs: { type: string }[] }[] import { useReadContract } from 'wagmi' const { data } = useReadContract({ address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', abi: erc721Abi, functionName: 'balanceOf', // 无补全、无检查 args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], })

Const assertion 最直接的收益是编译期捕获拼写错误:

// ✅ const-asserted abi 下,错误的函数名直接报错 const erc721Abi = [ { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ type: 'address', name: 'owner' }], outputs: [{ type: 'uint256' }], }, // ...(其余条目同上) ] as const import { useReadContract } from 'wagmi' useReadContract({ abi: erc721Abi, functionName: 'balanecOf', // ❌ 编译错误:不在 ABI 的函数名联合中 })

确保 ABI 与 Typed Data 定义方式正确,既能阻止运行时错误,也能显著提升开发效率。

配置内部类型(高级)

在高级场景下,你可能需要配置 wagmi 的内部类型。wagmi 中与 ABI 和 EIP-712 Typed Data 相关的大多数类型由 ABIType 驱动,更多类型配置细节可参考 ABIType 官方文档(abitype.dev)。

小结:类型安全的三层保障

层次机制源码依据
编译器基线tsconfig.json开启strict: true,TypeScript>=5.9.3packages/react/package.json
Config 层declare module 'wagmi'声明合并Register,或 hook 显式传入config属性register.ts、useConfig.ts
ABI 层const-asserted / 内联 ABI,const泛型参数驱动 functionName、args 全量推断useReadContract.ts

三点实践建议:其一,新项目中优先采用声明合并(官方脚手架 create-wagmi 模板 即此写法),多 config 场景再用config属性;其二,升级 wagmi 或 TypeScript 时把类型变更当作可能的破坏点来回归验证;其三,所有 ABI 一律as const,无法内联时用 CLI 生成,避免 JSON 导入丢失字面量类型。

【免费下载链接】wagmiReactive primitives for Ethereum apps项目地址: https://gitcode.com/GitHub_Trending/wa/wagmi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询