'Narrow return type of typescript function based on provided key of struct
I have *Response data structures which represents the result of an API call:
type Question = {
questionId: string
}
type QuestionsResponse = {
questions: Question[],
pagination: Pagination
}
type User = {
userId: string
}
type UsersResponse = {
users: User[],
pagination: Pagination
}
and a type-safe method that lets me get them using:
const usersResponse = getData<UsersResponse>('/v1/users') // UsersResponse
const questionsResponse = getData<QuestionsResponse>('/v1/questions') // QuestionsResponse
I want to write a type-safe method that takes multiple responses of the same type, and a key and returns a concatenation of all the data at that key in each struct. Something like:
getCombined<T = {}>(responses: T[], key keyof T) => {
let all = []
responses.forEach((response) => {
all = all.concat(response[key])
})
return all
}
which I want to call like:
const users = getCombined([usersResponseA, usersResponseB], 'users')
const questions = getCombined([questionsResponseA, questionsResponseB], 'questions')
e.g.:
usersResponses: UsersResponse[] = [{
users: [{userId: "a"}, {userId: "b"}],
pagination: {}
}, {
users: [{userId: "c"}],
pagination:{}
}]
const users = getCombined(usersResponses, "user") // should be [{userId: "a"}, {userId: "b"}, {userId: "c"}] of type User[]
This works, but there are implicit anys in the function, and the type of users and questions is any[], where I would like them to be User[] and Question[] respectively.
I can workaround that with:
const users: User[] = getCombined([usersResponseA, usersResponseB], 'users')
const questions: Question[] = getCombined([questionsResponseA, questionsResponseB], 'questions')
But that doesn't provide the level of type safety I'd like.
How can I narrow the type of the result of getCombined() to the type of identified object array, based on the supplied key?
let all: UDT[typeof key] = [] isn't allowed because [] isn't assignable to Pagination, and in any case would change the result type to Pagination | Question[].
I tried setting the result type to UDT[typeof key][], but that's wrong because it gives, for example, (Pagination | Question[])[]
I've tried various combinations of Extract, infer, keyof, typeof and discrimination, but haven't had any luck. Is what I'm trying to do even possible?
Solution 1:[1]
Firstly, I'd use K extends keyof T to determine key of the current generic type (the document for keyof)
And then I'd use infer to unbox your array from T[K][] to U[]. In your case, it can be User[][] to User[].
const getCombined = <T, K extends keyof T>(responses: Array<T>, key: K) => {
// if T[K] is not an array, it will keep the original type of T[K]
// if T[K] is an array, it will convert the array from Array<T[K]> to Array<U>
type Unboxed<TK> = TK extends (infer U)[] ? U : TK;
let all: Unboxed<T[K]>[] = []
responses.forEach((response) => {
const subArray = response[key] as Unboxed<T[K]>
all = all.concat(subArray)
})
return all
}
My full example here
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 |
