Reputation: 912
I have this interface
, which consists of different dynamic keys (API Routes / paths), followed by a method and the response.
interface ApiRoutes {
"auth/login": {
POST: {
response: {
username: string;
token: string;
};
};
};
"users/me": {
GET: {
response: {
username: string;
name: string;
age: number;
};
};
};
}
I'm trying to write a type that will extract a response given the path
and the method
. My approach was the following:
type ApiResponse<
Path extends keyof ApiRoutes,
Method extends keyof ApiRoutes[Path]
> = ApiRoutes[Path][Method]["response"];
But the compiler complains, saying that "response"
can't be used to index
Type '"response"' cannot be used to index type 'ApiRoutes[Path][Method]'.ts(2536)
Oddly enough, if I ignore the error, VS Code will still show suggestions/autocomplete the response object as if nothing happened.
const response: ApiResponse<"auth/login", "POST"> = {
username: "foo",
token: "bar",
};
Reference
Upvotes: 4
Views: 312
Reputation: 327614
As @futur says, this is a backlogged compiler bug at microsoft/TypeScript#21760. But I would avoid //@ts-ignore
except as a last resort.
Instead, my go-to workaround when the compiler loses track of some intended constraint of a generic type is to use the Extract<T, U>
utility type to remind it. Let's say I have some type expression T
that depends on some as-yet unspecified generic type parameters. I know that T
is assignable to U
but the compiler can't see it for some reason.
Then, instead of writing T
, I write Extract<T, U>
. The compiler will concede that Extract<T, U>
is assignable to U
no matter what T
is. Later, when the type parameters in T
are specified, as long as it turns out that I was right about T
being assignable to U
, the type Extract<T, U>
will evaluate to just T
as desired. (If I was wrong however, then it will evaluate to never
, which will probably not be what I wanted... so I should be careful.)
In your case you have ApiRoutes[Path][Method]
and you'd like it to be seen as something with a response
key. So it should be assignable to {response: any}
(or maybe {response: object}
or {response: SomeResponseType}
). If we use Extract<ApiRoutes[Path][Method], {response: any}>
, now the compiler will let you index into it with "response"
:
type ApiResponse<
Path extends keyof ApiRoutes,
Method extends keyof ApiRoutes[Path]
> = Extract<ApiRoutes[Path][Method], { response: any }>["response"];
Now there's no error. And you can check that it works as expected:
type R = ApiResponse<"auth/login", "POST">;
/* type R = {
username: string;
token: string;
} */
const response: ApiResponse<"auth/login", "POST"> = {
username: "foo",
token: "bar",
};
Upvotes: 3
Reputation: 6049
Credit to @futur answer
One of the comments in the issue that @futur has shared is exactly the reason for the cause of the problem.
The nut of the problem is that the base constraint of
U extends keyof IExample[T]
is a string, even though we would like it to be "bar". This means thatIExample[T][U]
doesn't have a constraint, and therefore can't be indexed by anything exceptkeyof IExample[T][U]
, which "baz" is not.
In your case, the constraint Method extends keyof ApiRoutes[Path]
is a string which means that ApiRoutes[Path][Method]
doesn't have any constraint.
A simple workaround for your problem would be
type ApiResponse<
Path extends keyof ApiRoutes,
Method extends keyof ApiRoutes[Path],
Response extends keyof ApiRoutes[Path][Method]
> = ApiRoutes[Path][Method][Response];
const response: ApiResponse<"auth/login", "POST", 'response'> = {
username: "foo",
token: "bar",
};
We also need to constraint the string response
to keyof ApiRoute[Path][Methiod]
which is a string literal response
.
Upvotes: 1
Reputation: 1843
Unfortunately, this appears to be a known issue, with no clear timeline on resolution.
In essence, TypeScript seems to "forget" that Method
is keyof
one of ApiRoutes
' nested objects. You can see this for yourself if you try adding in a third generic, which I'm calling Response
in the following example.
type ApiResponse<
Path extends keyof ApiRoutes,
Method extends keyof ApiRoutes[Path],
Response extends keyof ApiRoutes[Path][Method] = 'response'
> = ApiRoutes[Path][Method]['response'];
This will result in the following error:
Type 'string' is not assignable to type 'keyof ApiRoutes[Path][string]'.
^^^^^^
Note that it states string
for the type of Method
, indicating that the compiler sees no relation between it and a nested object within ApiRoutes
.
The only real way to resolve this, for the time being, seems to be to @ts-expect-error
or @ts-ignore
the last line of your type declaration.
Upvotes: 4