Hardhat + TypeChainで型の付いたContractを呼び出す

Hardhat + TypeChainで型の付いたContractを呼び出す

Slug
hardhat-typechain-typed-contract
Tags
Web3
Node.js
TypeScript
Published
February 18, 2022
早くこの仕様を知りたかった。今までの醜いworkaroundは何だったのか...。そんなの知ってるよって人のほうが多いかもしれません。

良くない例

僕らの強い味方Hardhatですが、TypeChainと一緒に使っている人も多いのではないでしょうか。
もしかすると、以下のようなコードを見たことはありませんか?
 
hardhat-ethersで取得したContractに型を上書き
import { ethers } from 'hardhat'; import { IERC20 } from '../typechain-types'; const ADDRESS = '0x6121191018BAf067c6Dc6B18D42329447a164F05'; async function main() { const erc20 = (await ethers.getContractAt('IERC20', ADDRESS)) as IERC20; const totalSupply = await erc20.totalSupply(); console.log(totalSupply); } main();
 
TypeChainが生成したFactoryファイルを参照
import { ethers } from 'hardhat'; import { IERC20__factory } from '../typechain-types'; const ADDRESS = '0x6121191018BAf067c6Dc6B18D42329447a164F05'; async function main() { const signer = await ethers.getSigners(); const erc20 = IERC20__factory.connect(ADDRESS, signer[0]); const totalSupply = await erc20.totalSupply(); console.log(totalSupply); } main();
 
特に前者はHardhat + Typechainを導入したrepoを見ているとたまに遭遇するんですが、なんでこういうコードが発生するかというと、hardhat-ethersがデフォルトでは ContractFactory を返すため、せっかくTypeChainで型を生成したのにいざ使うときには不完全な型付けになってしまうという本末転倒な状況になるからです。
export declare function getContractFactory( name: string, signerOrOptions?: ethers.Signer | FactoryOptions ): Promise<ethers.ContractFactory>;
 
getContractFactory で取得したContractは自動で型付けしてよドラえも〜んと思っていたのですが、既にありました。

解決策

tsconfig.json でTypeChainのoutput directoryをincludeする。

./typechain-types がTypeChainのoutput directoryです。
{ "compilerOptions": { "target": "es5", "module": "commonjs", "strict": true, "esModuleInterop": true, "outDir": "dist", "resolveJsonModule": true }, "include": ["./scripts", "./test", "./typechain-types"], "files": ["./hardhat.config.ts"] }
 
元のコードに戻りましょう。あら不思議。こんなにシンプルになりました。
import { ethers } from 'hardhat'; const ADDRESS = '0x6121191018BAf067c6Dc6B18D42329447a164F05'; async function main() { const erc20 = await ethers.getContractAt('IERC20', ADDRESS); const totalSupply = await erc20.totalSupply(); console.log(totalSupply); } main();
 
ちゃんと IERC20 型で返ってきてますね。
notion image

参照元

以下のissueを読んでいる時に気づきました。
よく見たらREADMEにもincludeするような例が書いてあります。ただしこちらは理由については触れられていないのでissueで気がついてよかったです。

中で何が起こっているか

TypeChainが hardhat.d.ts というファイルを生成して、HardhatのInterfaceをオーバーライドしています。これが、TypeChainのoutputディレクトリをincludeしないといけない所以ですね。
 
// hardhat.d.ts /* Autogenerated file. Do not edit manually. */ /* tslint:disable */ /* eslint-disable */ import { ethers } from "ethers"; import { FactoryOptions, HardhatEthersHelpers as HardhatEthersHelpersBase, } from "@nomiclabs/hardhat-ethers/types"; import * as Contracts from "."; declare module "hardhat/types/runtime" { interface HardhatEthersHelpers extends HardhatEthersHelpersBase { getContractFactory( name: "Counter", signerOrOptions?: ethers.Signer | FactoryOptions ): Promise<Contracts.Counter__factory>; getContractAt( name: "Counter", address: string, signer?: ethers.Signer ): Promise<Contracts.Counter>; // default types getContractFactory( name: string, signerOrOptions?: ethers.Signer | FactoryOptions ): Promise<ethers.ContractFactory>; getContractFactory( abi: any[], bytecode: ethers.utils.BytesLike, signer?: ethers.Signer ): Promise<ethers.ContractFactory>; getContractAt( nameOrAbi: string | any[], address: string, signer?: ethers.Signer ): Promise<ethers.Contract>; } }
 
HardhatのExampleを使えば上と同じTypechain typesを生成できますので、気になる方は自分で動かしてみてください。