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>
?
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/"