Returning an Array in flatMap seems to produce the wrong TypeScript type

Advertisements

I’m assuming this is user error on my part, but when I write the code below, I’m expecting the return type to be User[] but TS seems to think it’s User[][] even though I’m calling flatMap(). I’m not sure if my generics are messing something up, but it seems like TS doesn’t grok that the result will be flattened by one level. The actual output is correct, but doesn’t match the inferred type.

type User = {
  id: number
  firstName: string
}

type UserResponse = {
  items: User[]
  nextCursor: number
  hasNextPage: boolean
}

type InfiniteData<PageType> = {
  pages: PageType[]
}

const data: InfiniteData<UserResponse> = {
  pages: [
    {
      items: [
        {id: 1, firstName: 'Bob'},
        {id: 2, firstName: 'Alice'}
      ],
      nextCursor: 1,
      hasNextPage: false
    }
  ]
}

function useInfiniteData<TData, TValue>(
  data: InfiniteData<TData>,
  selectFn: (page: TData) => TValue
) {
  return data.pages.flatMap((page) => {
    // Returns a User[]
    return selectFn(page)
  })
}

// Expect User[] because flatMap flattens by one level,
// but TS thinks it's User[][]
const result = useInfiniteData(data, (page) => page.items)
// Output:
// result = [{
//   "id": 1,
//   "firstName": "Bob"
// }, {
//   "id": 2,
//   "firstName": "Alice"
// }] 

Here is a TS Playground link if that helps with debugging. Appreciate any advice!

>Solution :

What’s happening is that Typescript infers the function’s return type from its declaration, it does not infer a different return type at each call-site. Rather, there is one generic return type, and then that is realised at each call-site by plugging in what the type parameters are for that call-site.

The function’s return type is inferred as TValue[], because you call flatMap with the parameter selectFn and that returns TValue. Just looking at the function declaration, Typescript has no way of knowing that TValue is an array type, and indeed the signature doesn’t require it to be one. Perhaps a cleverer compiler could infer TValue extends (infer U)[] ? U[] : TValue[] as the return type, or something similar, but flatMap does not have an overload with such a signature, and Typescript does not invent new conditional types during inference.

Then at the call-site, the parameter TValue is inferred to be User[] because that’s what the selectFn argument returns, and therefore the return type TValue[] ends up being User[][], which is not what you want.

The simplest solution is to declare that selectFn returns either a value or an array:

function useInfiniteData<TData, TValue>(
  data: InfiniteData<TData>,
  selectFn: (page: TData) => TValue[] | TValue
) {
   // ...
}

Playground Link

Leave a ReplyCancel reply