Search code examples
typescript

Typescript can't use value to index object type that satisfies another type with that value


Sorry for the convoluted title.

I have this in typescript:

type EthereumAddress = `0x${string}`

type Chain = 'eth'

type ApiMethods = {
  getToken(args: {
    tokenAddress: EthereumAddress
  }): string

  getRank(args: {
    tokenAddress: EthereumAddress
  }): string
}

type ApiPaths<M extends ApiMethods> = {
  [K in keyof M]: {
    path: string
    chain?: Chain
  }
}

const apiPaths = {
  getToken: {
    path: 'tokens/',
    chain: 'eth'
  },

  getRank: {
    path: 'rank/',
  }
} as const satisfies ApiPaths<ApiMethods>

I would like to construct a Paths type which is a union of all possible paths the api can take. If the apiPaths has a chain parameter, the path should be ${path}/${chain}, if not ${path}.

I tried doing this:

type Paths<T extends typeof apiPaths = typeof apiPaths> = {
  [K in keyof T]: T[K] extends {chain: Chain}
    ? `${T[K]['path']}${T[K]['chain']}/`
    : `${T[K]['path']}`
}[keyof T]

But I get the error:

Type 'T[K]["path"]' is not assignable to type 'string | number | bigint | boolean | null | undefined'.ts(2322)
Type '"path"' cannot be used to index type 'T[K]'.

Why is path not a recognised parameter of T[K] if it is clearly mentioned in all apiPaths values and especially if apiPaths satisfies ApiPaths<ApiMethods>?


Solution

  • The problem with your Paths<T> type is that T is a generic type constrained to typeof apiPaths. That means it might be some extension of typeof apiPaths with extra properties not present in typeof apiPaths, and nothing requires those properties to have a path property. For example:

    type ApiPathsWithRaisins = typeof apiPaths & {raisins: true};
    type Oops = Paths<ApiPathsWithRaisins>; 
    

    Since Paths<T> is defined as

    type Paths<T extends typeof apiPaths = typeof apiPaths> = {
      [K in keyof T]: T[K] extends { chain: Chain }
      ? `${T[K]['path']}${T[K]['chain']}/`
      : `${T[K]['path']}`
    }[keyof T]
    

    then for Paths<ApiPathsWithRaisins>, K will iterate over "getToken" | "getRank" | "raisins". And when K is raisins, T[K] is true, and true does not have a known member named path. So you cannot safely assume that T[K] will have a path property.

    That's the source of the error. Again, typeof apiPaths is known to have a path property at every key, but T does not.


    You could fix it in any number of ways. If all you care about is computing Paths<typeof apiPaths> then you don't actually need generics at all. Just substitute typeof apiPaths instead of T and it will work:

    type MyApiPaths = typeof apiPaths;
    type Paths = {
      [K in keyof MyApiPaths]: MyApiPaths[K] extends { chain: Chain }
      ? `${MyApiPaths[K]['path']}${MyApiPaths[K]['chain']}/`
      : `${MyApiPaths[K]['path']}`
    }[keyof MyApiPaths]
    // type Paths = "rank/" | "tokens/eth/"
    

    Or perhaps even something like

    type Paths = keyof { [T in MyApiPaths[keyof MyApiPaths]
      as T extends { chain: infer C extends Chain } ? `${T["path"]}${C}/` : T['path']]: 0
    }
    // type Paths = "rank/" | "tokens/eth/"
    

    Or, if you do want to use generics, you just need to be sure that it is properly constrained to something where every property has a string-like path property:

    type GoodPaths<T extends Record<keyof T, { path: string }> = typeof apiPaths> = {
      [K in keyof T]: T[K] extends { chain: Chain }
      ? `${T[K]['path']}${T[K]['chain']}/`
      : `${T[K]['path']}`
    }[keyof T]
    type OK = GoodPaths;
    // type OK = "rank/" | "tokens/eth/"
    

    Playground link to code