I am trying to create a union of string literals that are deeply nested.
Mostly, the structure looks like this:
typechainTypes.factories.contracts
|
|----->Lock__factory (extends ContractFactory)
| |-----> contractName: string
|----->****** (this is a `export * as foo from bar`)
|-----> ****** (want to extract only if it extends ContractFactory; Extract<Foo, ContractFactory> type didn't work)
|-----> contractName: stringcontractName: string
Pseudocode of what I want (where ** is any depth):
type ContractFactories = typechainTypes.factories.contracts.**.* extends ContractFactory ? ___ : never;
type ContractNames = ContractFactories['contractName'];
Put differently:
type LookForFactory<T> = T.flatMap(foo => {
if (foo extends ContractFactory)
return foo;
else if (foo === undefined || foo === null || foo is SomePrimitive)
return never;
else
return LookForFactory<foo>;
}
type ContractNames = LookForFactory<typechainTypes.factories.contracts>;
That is, it should be a union of:
typechainTypes.factories.contracts.Lock__factory.contractName
typechainTypes.factories.contracts.SomethingA.this_is_a__factory.contractName
typechainTypes.factories.contracts.SomethingB.SomethingC.this_is_a__factory.contractName
typechainTypes.factories.contracts.*.*.*.*.this_is_a__factory.contractName
If having an unknown depth is impossible, I'd settle for a union of:
typechainTypes.factories.contracts.Lock__factory.contractName
typechainTypes.factories.contracts.UnknownNameA.UnknownNameB__factory.contractName
Incidentally, the original way I was trying to do this was using Parameters<ethers.getContractFactory>[0]
, but due to the final overload having any
as parameter 0, this ends up being any[]
, which isn't useful, and I haven't found a way to exclude the any[]
override.
Thanks!
Edit: In response to Alex's answer below (since this won't all fit in a comment): Alex proposed
type LookForFactory<T> = {
[K in keyof T & string]:
T[K] extends ContractFactory
? K
: `${K}.${LookForFactory<T[K]>}`
}[keyof T & string]
This seems almost right, but not quite.
Doing type ContractNames = LookForFactory<typeof typechainTypes.factories.contracts>;
results in type ContractNames = "somethingA.metadata.opensea.OpenSeaSchema__factory.prototype" | "somethingA.utils.JSON__factory.prototype" | "somethingA.utils.JsonTest__factory.prototype" | ... 10 more ... | "Lock__factory.prototype";
I think there are two problems with this: first, I'm getting a fully qualified name, and not the string literal itself (that .prototype
probably needs to be replaced with ['contractName']
somehow). The other problem is that apparently not all of the "factories" directly extend ContractFactory, so rather than checking whether something extends ContractFactory, I think we need to check whether a contractName
key exists.
To elaborate on that second bit:
typechainTypes.factories.contracts.somethingA.AbstractRuleCascade__factory
resolves to something that has a contractName
property but doesn't actually extend BaseFactory.
I tried this, which got closer:
type LookForFactory<T> = {
[K in keyof T & string]:
K extends 'contractName'
? T[K]
: LookForFactory<T[K]>
}[keyof T & string];
type ContractNames = LookForFactory<typeof typechainTypes.factories.contracts>;
which results in type ContractNames = LookForFactory<typeof typechainTypes.factories.contracts.somethingA> | "AppProtocolGame" | LookForFactory<typeof typechainTypes.factories.contracts.guessanumbergame> | LookForFactory<...> | "Lock"
The visible possibilities that don't say LookForFactory there look correct. So it's very close.
It's worth noting that typechainTypes.factories.contracts.somethingA
on mouseover says import somethingA
; if I hit F12 on contracts
it shows that it comes from export * as somethingA from "./somethingA";
Second edit: This seems to work, but I'm not sure why adding NonNullable "fixes" it (other no-op utility types seem to similarly "fix" it):
type LookForFactory<T> = NonNullable<{
[K in keyof T & string]:
K extends 'contractName'
? T[K]
: LookForFactory<T[K]>
}[keyof T & string]>;
results in type ContractNames = "OpenSeaSchema" | "JSON" | "JsonTest" | "AppGateway" | "LApp" | "SanctionedRegistry" | "TurnsLimitRule" | "TurnsRule" | "AppProtocolGame" | "GuessANumber" | "GuessANumberRuleCascade" | "TicTacToe" | "TicTacToeRuleCascade" | "Lock"
I think you want this:
This is a recursive conditional type.
string
keys asK
of the object typeT
.T[K]
is aContractFactory
, then return that key.T[K]
is anything else, then return${K}.${LookForFactory<T[K]>}
which merges the parent keyK
with all matches in theT[K]
object.[keyof T & string]
at the end indexes the object we just made by it's own keys to get the value types of that object as a union.Let's try it out:
See Playground