reactjstypescriptreact-nativegenericsflatlist

A way to infer types based on the return value of the function in react prop


So i have this component definition that accepts two generics

function AsyncFlatList<ApiResponse, Item>({}: {
  url: string;
  transformResponse: (response: ApiResponse) => Item[];
  keyExtractor: (item: Item) => string;
}) {
  // ignore the implementation
  return <></>;
}

I call the component like this

type Post = {
    id: number
    title: string
    body: string
    userId: number
}

type PostMapped = {
   id: string
   title: string
   body: string
}

<AsyncFlatList<Post[], PostMapped>
  url="https://jsonplaceholder.typicode.com/posts"
  transformResponse={(response) => response}
  // The type of post params below is coming from `PostMapped`
  keyExtractor={(post) => post.id}
/>

The problem is that I don't want to define PostMapped explicitly. I want it to be inferred from the type of the return value on the transformResponse prop.

So that i can use the component like this:

<AsyncFlatList<Post[]>
  url="https://jsonplaceholder.typicode.com/posts"
  transformResponse={(response) =>
     response.map((singleResponse) => ({
       id: singleResponse.id.toString(),
       title: singleResponse.title,
       body: singleResponse.body,
     }))
  }
 // post types below will be inferred based on what's return from transformResponse
 // that is why we can use post.id directly without types issue because it is already transformed to string
 // in the transformResponse prop above
  keyExtractor={(post) => post.id}
/>

and I don't have to define PostMapped anymore since it will be inferred.

Kindly help to achieve that. Thanks


Solution

  • To make the minimal changes to your code, here is an example.

    import React from 'react';
    
    function AsyncFlatList<ApiResponse, Item = unknown>({}: {
      url: string;
      transformResponse: (response: ApiResponse) => Item[];
      keyExtractor: (item: Item) => string;
    }) {
      // ignore the implementation
      return <></>;
    }
    type Post = {
        id: number
        title: string
        body: string
        userId: number
    }
    
    const MyList: React.FC = () => {
      return (
        <AsyncFlatList
        url="https://jsonplaceholder.typicode.com/posts"
        transformResponse={(response: Post[]) =>
            response.map((singleResponse) => ({
              id: singleResponse.id.toString(),
              title: singleResponse.title,
              body: singleResponse.body,
            }))}
        keyExtractor={(post) => post.id}
        />);
    }
    

    In fact, the ApiResponse generic argument is not needed in your case (but I don't know if you'll need it later in your implementation).


    Edit: After reading your comment I think I better understand what result you want to really achieve: You want to define the contract of AsyncFlatList by passing the generic argument and inferring from the type of transformResponse at the same time.

    And It is not possible. You either explicitly pass the type arguments or let type argument inference do the job.

    Let's look at a small non-react example:

    //@ts-ignore
    function testing<A,B>(cb1: (a:A)=>B, cb2: (b:B)=>string);
    testing((a: Post)=>a, post=>post.body); // pass typecheck
    testing<Post>(a=>a, post=>post.body); // fail typecheck, beacuse B is missing
    
    //@ts-ignore
    function testing<A=unknown,B=unknown>(cb1: (a:A)=>B, cb2: (b:B)=>string);
    testing((a: Post)=>a, post=>post.body); // pass typecheck
    testing<Post>(a=>a, post=>post.body); // fail typecheck, beacuse B is unknown by default