'Indexing object with a generic key
First time I've run into this issue so apologies if the title doesn't make sense, couldn't figure out a good way to word it.
I'm working with an API that returns a response like this:
{
PaginatedResults: {
TotalResults: 100,
Offset: 0,
Limit: 0,
PageSize: 100,
},
Error: {
Message: '',
Code: '',
},
Customers: [{...}]
}
Where Customers can be replaced with any type the API may return. So it could also look something like this:
{
PaginatedResults: {
TotalResults: 100,
Offset: 0,
Limit: 0,
PageSize: 100,
},
Error: {
Message: '',
Code: '',
},
Sales: [{...}]
}
I currently express the type like this:
type PaginatedResponse<TResponse extends object> =
TResponse &
Readonly<{
PaginatedResponse: {
TotalResults: number;
Offset: number;
Limit: number;
PageSize: number;
};
Error?: {
Message: string;
Code: string;
};
}>;
This has been working fine but I recently ran into a issue when I was trying to create a generic helper function to auto paginate an endpoint.
To keep track of all the results I need to do something like this:
const allResults = [...initialResults];
const nextResp = await apiCall();
allResults.push(...nextResp['Customers'])
But the problem is the key 'Customers' can be any of the 50+ types the API may return. So I need a generic way to index into the response. I've tried a bunch of different things with no success so I'm looking to get some guidance on the best way to approach something like this.
Solution 1:[1]
TypeScript alone will not be too helpful here. In ...nextResponse['Customers'] you are expecting TypeScript to fill in the correct key. But what the correct key is will only be known either ar runtime or by a manually supplied generic type.
But maybe this will help you anyway:
// I added some generic types here which will help later
type PaginatedResponse<TResponse extends {[key in TKey]: any}, TKey extends keyof TResponse = keyof TResponse> =
Readonly<{
PaginatedResponse: {
TotalResults: number,
Offset: number,
Limit: number,
PageSize: number,
};
Error?: {
Message: string;
Code: string;
};
}> & TResponse
Define possible Response types:
type Response1 = PaginatedResponse<{Customers: any}>
type Response2 = PaginatedResponse<{Sales: any}>
type AllResponses = Response1 | Response2
Now let's define a function which gets a PaginatedResponse and pushes the TResponse in an array:
type GetResponseKey<T> = T extends PaginatedResponse<infer TRes, infer TKey> ? TKey : never
const arr: any[] = []
function pushToResponseArray<
T extends AllResponses,
K extends GetResponseKey<T>
>(response: T, key: K){
arr.push(response[key])
}
const res: Response1 = {} as any
pushToResponseArray(res, "Customers") // We have to manually specify the key here
// but typescript will check if key is correct
This is not a perfect solution and I think you always have to either manually set the key or determine it with some JavaScript logic.
To sum it up:
Somewhere in your application you need some JavaScript logic to determine the type of the PaginatedResponse. Map the type to the corresponding key afterwards to access the TResponse.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
