早くこの仕様を知りたかった。今までの醜い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
型で返ってきてますね。参照元
以下の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を生成できますので、気になる方は自分で動かしてみてください。