
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 配置方案、源码级类型推断链路ResolvedRegister、ConfigParameter、hook 泛型约束并能落地 const assertion 让 ABI 拼写错误在编译期即被捕获。TypeScript 版本要求与 strict 模式wagmi 的设计目标是尽可能类型安全as type-safe as possible。从 packages/react/package.json 的peerDependencies可以确认当前的硬性要求typescript:5.9.3且声明为optional即仅在使用类型时需要react:18viem:2.xABI 与 Typed Data 类型推断的底层引擎tanstack/react-query:5.0.0关于类型相关的版本管理有几点必须牢记TypeScript 不遵循 semverminor 版本发布经常引入破坏性变更。wagmi 仓库中类型层面的变更被视为非破坏性通常以 patch 版本发布——否则每一次类型增强都得发布一个 major 版本。强烈建议将wagmi和typescript锁定到具体的 patch 版本并在升级时预期类型可能被修复或升级。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。这解释了为什么未注册时chainId是number而非具体联合类型。设置方法在项目中添加如下声明。下面的示例把声明合并与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会基于config的chains进行类型推断。从 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: Configreadonly [typeof mainnet, typeof sepolia] } } import { useBlockNumber } from wagmi useBlockNumber({ chainId: 123 }) // ❌ 编译错误123 不在 config 的 chains 中仓库中的类型测试 packages/register-tests/react/src/config.ts 验证了同一模式注册包含celo、mainnet、optimism、zkSync四条链的 config 后ChainId类型即被推导为这四个链 id 的联合类型。方式二Hook 的config属性当你拥有多个 Wagmiconfig或不想使用声明合并时可以直接把特定config传给 hook。config属性在类型层的定义来自 packages/core/src/types/properties.tsexport type ConfigParameterconfig extends Config Config { config?: Config | config | undefined }每个支持该参数的 hook 参数类型都会与ConfigParameterconfig做交叉。例如useReadContract的参数类型useReadContract.ts就是ReadContractOptions... ConfigParameterconfig。运行时的解析逻辑则非常直白见 useConfig.tsexport function useConfigconfig extends Config ResolvedRegister[config]( parameters: UseConfigParametersconfig {}, ): UseConfigReturnTypeconfig { // 显式传入的 config 优先否则回退到 Context const config parameters.config ?? useContext(WagmiContext) if (!config) throw new WagmiProviderNotFoundError() return config as UseConfigReturnTypeconfig }定义两个不同链的 configimport { 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: Configreadonly [typeof mainnet] declare const configB: Configreadonly [typeof optimism] import { useBlockNumber } from wagmi useBlockNumber({ chainId: 123, config: configA }) // ❌ 编译错误 useBlockNumber({ chainId: 123, config: configB }) // ❌ 编译错误这种方式更显式适合不使用 React Context 或声明合并的高级场景多 config 并存、非 React 渲染层调用等。Const-Asserted ABI 与 Typed Datawagmi 能够基于 ABI 与 EIP-712 Typed Data 定义推断类型——底层由 viem 与 ABIType 驱动。这带来了从合约到前端的完整端到端类型安全以及显著的开发者体验提升自动补全 ABI 条目名、捕获拼写错误、推断参数与返回类型包括函数重载等。要让它工作必须const-assert ABI 和 Typed Data或者将它们内联定义。以useReadContract的abi配置参数为例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可解决这一痛点。文档中所有出现abi或types配置属性的地方基本都可以用 const-asserted 或内联的 ABI、Typed Data 获得类型安全与推断。下面是 useReadContract 在 const-assert 与未 assert 两种情况下的对比。源码层面useReadContract.ts 使用了const泛型参数const abi、const args保证abi字面量以只读元组形式保留functionName、args的约束均基于ContractFunctionNameabi, pure | view与ContractFunctionArgs...从 viem 推导// ✅ Const-AssertedfunctionName、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-assertabi 退化为宽泛的 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: trueTypeScript5.9.3packages/react/package.jsonConfig 层declare module wagmi声明合并Register或 hook 显式传入config属性register.ts、useConfig.tsABI 层const-asserted / 内联 ABIconst泛型参数驱动 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),仅供参考