Create nested string literal union when you don't know intermediary types or names

47 views Asked by At

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"

1

There are 1 answers

2
Alex Wayne On

I think you want this:

type LookForFactory<T> = {
  [K in keyof T & string]:
    T[K] extends ContractFactory
      ? K
      : `${K}.${LookForFactory<T[K]>}`
}[keyof T & string]

This is a recursive conditional type.

  1. First it maps over the string keys as K of the object type T.
  2. If T[K] is a ContractFactory, then return that key.
  3. If T[K] is anything else, then return ${K}.${LookForFactory<T[K]>} which merges the parent key K with all matches in the T[K] object.
  4. [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:

type ContractFactory = {
  contractName: string
}

type Typechain = {
  factories: {
    contracts: {
      Lock__factory: ContractFactory,
      SomethingA: {
        this_is_a__factory: ContractFactory,
        SomethingB: {
          this_is_a__factory: ContractFactory
        }
        not_a_factory: true
      }
    }
  }
}

type ContractNames = LookForFactory<Typechain>;
/*
"factories.contracts.Lock__factory" |
"factories.contracts.SomethingA.this_is_a__factory" |
"factories.contracts.SomethingA.SomethingB.this_is_a__factory"
*/

See Playground