Typescript: Nested typings - Looking for a more elegant solution

Viewed 42

I am building an API abstraction using Typescript Generics but am struggling to find an elegant syntax to get things working as I would like. Here is a dummied up version of what I have managed to get working. Any ideas of how to simplify things?

interface ExampleInterface {
  LevelOne: {
    LevelTwo: {
      keyOne: string;
      keyTwo: number;
      keyThree: boolean;
    }
  }
}

class ExampleClassOne <T1, K1 extends keyof T1> {
  requestMethod<K2 = 'LevelTwo'> (): Promise<K2 extends keyof T1[K1] ? T1[K1][K2] : any> {
    return new Promise (async (resolve, reject) => {
        try {
            const requestResponse = await axios({
                url: `https://someurl.com/api/`,
                method: 'GET'
            })
            resolve(requestResponse.data)
        } catch (err) {
            reject(err.response.status);
        }
    })
  }
}

class ExampleClassTwo {
  methodObj = {
    levelOne: new ExampleClassOne<ExampleInterface, 'LevelOne'>()
  }
}

In particular this bit of code is bothering me:

Promise<K2 extends keyof T1[K1] ? T1[K1][K2] : any>

I would like it to look more like this:

Promise<T1[K1][K2]>

I am still trying to get my head around Typescript and would appreciate some kind of explanation as to what kind of sorcery is playing out here. Why does it require this conditional type guard for the second level of nesting?

Many thanks.

1 Answers

The problem is the constraint of K2. In this case there is none:

class ExampleClassOne <T1, K1 extends keyof T1> {
  requestMethod<K2 = 'LevelTwo'> (): Promise<K2 extends keyof T1[K1] ? T1[K1][K2] : any> {

Here K2 could be any type because it's unconstrained. If omitted, it uses "LevelTwo" as a default, but it could be any string, a boolean, an object interface, a function, etc.

So typescript notices that you are trying to index an object by a type that is not guaranteed to be a key of that object and throws a type error. This snippet handles that with a conditional type that checks to make sure that K2 is a key that exists before using it to drill into T1[K1].


One great way to avoid conditionals is to provide better constraints. This way one branch of the conditional becomes a type error that the compiler won't allow, and then you don't need the conditional anymore.

So to fix it, K2 needs to be constrained just like K1 is, but one level deeper.

class ExampleClassOne <T1, K1 extends keyof T1> {
  requestMethod<K2 extends keyof T1[K1]> (): Promise<T1[K1][K2]> {

Playground

Related