Search code examples
reactjstypescript

How to propagate generics when using render prop function


I am currently creating a Compound Component type component. At that time I am looking for a way to propagate generics when using render prop.

interface ListContextValue<T> {
  rows: T[];
}

const createListContext = once(<T,>() =>
  createContext<ListContextValue<T> | undefined>(undefined)
);

const useListContext = <T,>() => {
  const context = useContext(createListContext<T>());
  if (!context) {
    throw new Error(
      "useListContext must be used within an ListContext.Provider"
    );
  }
  return context;
};

interface ListProps<T> {
  rows: T[];
  children?: React.ReactNode;
}

export const List = <T,>({ rows, children }: ListProps<T>) => {
  const ListContext = createListContext<T>();

  return (
    <ListContext.Provider value={{ rows }}>
      {children}
    </ListContext.Provider>
  );
};

export interface ListContainerProps<T> extends Omit<SlotProps, "children"> {
  children?: (props: { row: T }) => React.ReactNode;
}

export const ListContainer = <T,>({ children }: ListContainerProps<T>) => {
  const { rows } = useListContext<T>();

  return (
    <div>
      {rows.map((row) => {
        return children ? children({ row }) : null;
      })}
    </div>
  );
};

Above is the List component code.

export default function UserList() {
  return (
    <List rows={rows}>
      <ListContainer>
        {({ row }) => (
          <div>
            {/** row is unknown type */}
            {row}
          </div>
        )}
      </ListContainer>
    </List>
  );
}

When using the component, is it possible to automatically infer the row type in the render prop function?

export default function UserList() {
  return (
    <List<User[]> rows={rows}>
      <ListContainer<User>>
        {({ row }) => (
          <div>
            {row}
          </div>
        )}
      </ListContainer>
    </List>
  );
}

Of course, it can be solved this way, but I don't think it's a good way.


Solution

  • Unfortunately, TypeScript has no way to detect React Context: TS is based on the Function Component signature, i.e. its props; but it knows nothing about any Context used inside.

    In your very example, we can obviously get rid of the Context and directly map on the array items:

    export default function UserList() {
      return (
        <>
          {rows?.map((row) => (
            //        ^? (parameter) row: User
            <div>
              {row}
            </div>
          ))}
        </>
      );
    }
    

    Playground Link


    However, it is very probable that your example is oversimplified: you probably want to define your <ListContainer> Component separately, hence you resort to React Context.

    In that case, the easiest solution is indeed to explicitly specify the type parameter as you did (<ListContainer<User>>).