'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