Skip to content

Commit

Permalink
refactor: generics
Browse files Browse the repository at this point in the history
  • Loading branch information
tmm committed Aug 31, 2023
1 parent b76b727 commit dc42477
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-frogs-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": minor
---

Refactored generic types to be more strict.
21 changes: 14 additions & 7 deletions src/actions/getContract.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,10 +616,10 @@ test('argument permutations', async () => {
'function overloadedNonpayable2(string x, uint256 y)',

'function overloadedView() view returns (string)',
'function overloadedView(string x) view returns (string)',
'function overloadedView(string x, uint256 y) view returns (string)',
'function overloadedView(string x) view returns (address)',
'function overloadedView(string x, uint256 y) view returns (uint256)',
'function overloadedView2(string x) view returns (string)',
'function overloadedView2(string x, uint256 y) view returns (string)',
'function overloadedView2(string x, uint256 y) view returns (address)',

// events
'event WithoutInputs()',
Expand Down Expand Up @@ -660,10 +660,17 @@ test('argument permutations', async () => {
contract.read.viewWithArgs(['foo', 69n])
contract.read.viewWithArgs(['foo', 69n], { blockNumber: 123n })

contract.read.overloadedView()
contract.read.overloadedView(['foo'])
contract.read.overloadedView2(['foo'])
contract.read.overloadedView2(['foo', 69n])
const res1_1 = await contract.read.overloadedView()
const res1_2 = await contract.read.overloadedView(['foo'])
const res1_3 = await contract.read.overloadedView(['foo', 123n])
expectTypeOf(res1_1).toEqualTypeOf<string>()
expectTypeOf(res1_2).toEqualTypeOf<Address>()
expectTypeOf(res1_3).toEqualTypeOf<bigint>()

const res2_1 = await contract.read.overloadedView2(['foo'])
const res2_2 = await contract.read.overloadedView2(['foo', 69n])
expectTypeOf(res2_1).toEqualTypeOf<string>()
expectTypeOf(res2_2).toEqualTypeOf<Address>()

const read_1 = await contract.read.viewWithArgs(['foo', 69n])
expectTypeOf(read_1).toEqualTypeOf<string>()
Expand Down
58 changes: 39 additions & 19 deletions src/actions/getContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type { Transport } from '../clients/transports/createTransport.js'
import type { Chain } from '../types/chain.js'
import type {
AbiEventParametersToPrimitiveTypes,
ContractFunctionArgs,
ContractFunctionName,
MaybeExtractEventArgsFromAbi,
} from '../types/contract.js'
import type {
Expand Down Expand Up @@ -152,7 +154,9 @@ export type GetContractReturnType<
[FunctionName in _ReadFunctionNames]: GetReadFunction<
_Narrowable,
TAbi,
FunctionName
FunctionName extends ContractFunctionName<TAbi, 'pure' | 'view'>
? FunctionName
: never
>
}
}) &
Expand Down Expand Up @@ -668,30 +672,46 @@ export function getEventParameters(
return { args, options }
}

type OmittedReadProperties = 'abi' | 'address' | 'args' | 'functionName'

type GetReadFunction<
Narrowable extends boolean,
TAbi extends Abi | readonly unknown[],
TFunctionName extends string,
TAbiFunction extends AbiFunction = TAbi extends Abi
? ExtractAbiFunction<TAbi, TFunctionName>
: AbiFunction,
Args = AbiParametersToPrimitiveTypes<TAbiFunction['inputs']>,
Options = Prettify<
UnionOmit<
ReadContractParameters<TAbi, TFunctionName>,
'abi' | 'address' | 'args' | 'functionName'
>
>,
TFunctionName extends ContractFunctionName<TAbi, 'pure' | 'view'>,
> = Narrowable extends true
? (
...parameters: Args extends readonly []
? [options?: Options]
: [args: Args, options?: Options]
) => Promise<ReadContractReturnType<TAbi, TFunctionName>>
? <TArgs extends ContractFunctionArgs<TAbi, 'pure' | 'view', TFunctionName>>(
...parameters: TArgs extends readonly []
? [
options?: Prettify<
UnionOmit<
ReadContractParameters<TAbi, TFunctionName, TArgs>,
OmittedReadProperties
>
>,
]
: [
args: TArgs,
options?: Prettify<
UnionOmit<
ReadContractParameters<TAbi, TFunctionName, TArgs>,
OmittedReadProperties
>
>,
]
) => Promise<ReadContractReturnType<TAbi, TFunctionName, TArgs>>
: (
...parameters:
| [options?: Options]
| [args: readonly unknown[], options?: Options]
| [
options?: Prettify<
UnionOmit<ReadContractParameters, OmittedReadProperties>
>,
]
| [
args: readonly unknown[],
options?: Prettify<
UnionOmit<ReadContractParameters, OmittedReadProperties>
>,
]
) => Promise<ReadContractReturnType>

type GetEstimateFunction<
Expand Down
45 changes: 44 additions & 1 deletion src/actions/public/readContract.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ test('behavior', () => {
// @ts-expect-error Trying to use non-read function
functionName: 'approve',
})
assertType<void>(result)
assertType<string | bigint>(result)
})

test('without const assertion', async () => {
Expand Down Expand Up @@ -224,4 +224,47 @@ test('behavior', () => {
})
assertType<bigint>(result)
})

test('overloads', async () => {
const abi = parseAbi([
'function foo() view returns (int8)',
'function foo(address) view returns (string)',
'function foo(address, address) view returns ((address foo, address bar))',
'function bar() view returns (int8)',
])

const result1 = await readContract(publicClient, {
address: '0x',
abi,
functionName: 'foo',
})
assertType<number>(result1)

const result2 = await readContract(publicClient, {
address: '0x',
abi,
functionName: 'foo',
args: [],
})
assertType<number>(result2)

const result3 = await readContract(publicClient, {
address: '0x',
abi,
functionName: 'foo',
args: ['0x'],
})
assertType<string>(result3)

const result4 = await readContract(publicClient, {
address: '0x',
abi,
functionName: 'foo',
args: ['0x', '0x'],
})
assertType<{
foo: `0x${string}`
bar: `0x${string}`
}>(result4)
})
})
37 changes: 28 additions & 9 deletions src/actions/public/readContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import type { Transport } from '../../clients/transports/createTransport.js'
import type { BaseError } from '../../errors/base.js'
import type { Chain } from '../../types/chain.js'
import type {
ContractFunctionConfig,
ContractFunctionResult,
ContractFunctionArgs,
ContractFunctionName,
ContractFunctionParameters,
ContractFunctionReturnType,
} from '../../types/contract.js'
import {
type DecodeFunctionResultParameters,
Expand All @@ -22,14 +24,30 @@ import { type CallParameters, call } from './call.js'

export type ReadContractParameters<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = string,
TFunctionName extends ContractFunctionName<
TAbi,
'pure' | 'view'
> = ContractFunctionName<TAbi, 'pure' | 'view'>,
TArgs extends ContractFunctionArgs<
TAbi,
'pure' | 'view',
TFunctionName
> = ContractFunctionArgs<TAbi, 'pure' | 'view', TFunctionName>,
> = Pick<CallParameters, 'account' | 'blockNumber' | 'blockTag'> &
ContractFunctionConfig<TAbi, TFunctionName, 'view' | 'pure'>
ContractFunctionParameters<TAbi, 'pure' | 'view', TFunctionName, TArgs>

export type ReadContractReturnType<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = string,
> = ContractFunctionResult<TAbi, TFunctionName>
TFunctionName extends ContractFunctionName<
TAbi,
'pure' | 'view'
> = ContractFunctionName<TAbi, 'pure' | 'view'>,
TArgs extends ContractFunctionArgs<
TAbi,
'pure' | 'view',
TFunctionName
> = ContractFunctionArgs<TAbi, 'pure' | 'view', TFunctionName>,
> = ContractFunctionReturnType<TAbi, 'pure' | 'view', TFunctionName, TArgs>

/**
* Calls a read-only function on a contract, and returns the response.
Expand Down Expand Up @@ -65,7 +83,8 @@ export type ReadContractReturnType<
export async function readContract<
TChain extends Chain | undefined,
const TAbi extends Abi | readonly unknown[],
TFunctionName extends string,
TFunctionName extends ContractFunctionName<TAbi, 'pure' | 'view'>,
TArgs extends ContractFunctionArgs<TAbi, 'pure' | 'view', TFunctionName>,
>(
client: Client<Transport, TChain>,
{
Expand All @@ -74,8 +93,8 @@ export async function readContract<
args,
functionName,
...callRequest
}: ReadContractParameters<TAbi, TFunctionName>,
): Promise<ReadContractReturnType<TAbi, TFunctionName>> {
}: ReadContractParameters<TAbi, TFunctionName, TArgs>,
): Promise<ReadContractReturnType<TAbi, TFunctionName, TArgs>> {
const calldata = encodeFunctionData({
abi,
args,
Expand Down
9 changes: 6 additions & 3 deletions src/clients/decorators/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ import type { Account } from '../../types/account.js'
import type { BlockNumber, BlockTag } from '../../types/block.js'
import type { Chain } from '../../types/chain.js'
import type {
ContractFunctionArgs,
ContractFunctionConfig,
ContractFunctionName,
MaybeAbiEventName,
MaybeExtractEventArgsFromAbi,
} from '../../types/contract.js'
Expand Down Expand Up @@ -1283,10 +1285,11 @@ export type PublicActions<
*/
readContract: <
const TAbi extends Abi | readonly unknown[],
TFunctionName extends string,
TFunctionName extends ContractFunctionName<TAbi, 'pure' | 'view'>,
TArgs extends ContractFunctionArgs<TAbi, 'pure' | 'view', TFunctionName>,
>(
args: ReadContractParameters<TAbi, TFunctionName>,
) => Promise<ReadContractReturnType<TAbi, TFunctionName>>
args: ReadContractParameters<TAbi, TFunctionName, TArgs>,
) => Promise<ReadContractReturnType<TAbi, TFunctionName, TArgs>>
/**
* Sends a **signed** transaction to the network
*
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,11 @@ export type {
InferEventName,
InferFunctionName,
InferItemName,
///
ContractFunctionName,
ContractFunctionArgs,
ContractFunctionParameters,
ContractFunctionReturnType,
} from './types/contract.js'
export type {
AccessList,
Expand Down
Loading

0 comments on commit dc42477

Please sign in to comment.