0

I'm trying to type a property of a React component using generics. The problem is when I try to render the component as a render prop through another component, it seems to no longer be able to "infer" the correct type - instead it defaults to unknown and I have to use "as any" to make things work.

I've put together a working example here. Note the comments and "as any"'s: https://stackblitz.com/edit/react-ts-4oyi3d?file=index.tsx

Here's the code:

type TabProps<T> = {
  data?: T;
};

type TabsProps<T> = {
  children: ({
    childrenArr
  }: {
    childrenArr: React.ReactElement<TabProps<T>>[];
  }) => React.ReactElement<TabProps<T>>[];
  items: React.ReactElement<TabProps<T>>[];
};

type Data = { hello: string };

const Tab = <T extends unknown>({ data }: TabProps<T>) => <div>...</div>;

const Tabs = <T extends unknown>({ children, items }: TabsProps<T>) => {
  const childrenArr = useMemo(() => (Array.isArray(items) ? items : [items]), [
    items
  ]);

  // ...
  // In real application this is where manipulation of the elements in the childrenArr would take place
  // Since this is just an example I'll just do this
  const manipulatedchildrenArr = childrenArr.map(child => {
    return {
      ...(child as any),
      props: { data: { hello: "world is manipulated" } }
    };
  });

  return (
    <div>
      <div>Hello</div>
      <div>
        {typeof children === "function"
          ? children({ childrenArr: manipulatedchildrenArr })
          : children}
      </div>
    </div>
  );
};

const App = () => {
  const data: Data = { hello: "World" };

  return (
    <div className="App">
      <Tabs items={[<Tab data={data} />]}>
        {({ childrenArr }) =>
          childrenArr.map(child => (
            // Remove "as any" and it will be type unknown and result in an error
            <div>{(child.props as any).data.hello}</div>
          ))
        }
      </Tabs>
    </div>
  );
};

As you can see the type of the data prop is lost.

3
  • 2
    I working on a solution right now but, you could add a little more detail on what your end goal is for this code? Commented Feb 2, 2021 at 11:56
  • Btw, this might help you catchts.com/react-return-type , catchts.com/typed-react-children Commented Feb 2, 2021 at 12:07
  • @Nicholas_Jones cool. The reason for the data prop is that I sometimes need to use the data within it to conditionally render a custom tab label after it has been passed back from the render props. That is why I store it in a property in the component. The reason for sending it through a render prop is in this case to split up the elements into multiple arrays of elements such as visible / dropdown (for overflowing tabs) etc. Looking forward to seeing your solution! Commented Feb 2, 2021 at 12:35

1 Answer 1

1

Now I'm not sure if I went outside the scope of what you were looking for and If I did please let me know and I'll adjust the solution..

Update: I forgot to add code for Single tab.

import React from "react";
import ReactDOM from "react-dom";


export interface ITabProps<T> {
  data?: T;
  handleProcessData: (data: T) => string;
}

export function Tab<T>(props: ITabProps<T>) {
  const data = props.data ? props.handleProcessData(props.data) : "None";
  return <div>Hello {data}</div>;
}


export type TabElement<T> = React.ReactElement<ITabProps<T>> | React.ReactElement<ITabProps<T>>[]


export interface ITabsProps<T> {
  handleManipulation: (data: T) => T;
  children: TabElement<T>
}

export function Tabs<T>(props: ITabsProps<T>) {
  const array = [] as TabElement<T>[];
  if (Array.isArray(props.children))
    props.children.forEach((child) => {
      let mChild = <Tab<T> handleProcessData={child.props.handleProcessData} data={props.handleManipulation(child.props.data)}  /> as TabElement<T>;
      array.push(mChild)
    })
  else {
    let mChild = <Tab<T> handleProcessData={props.children.props.handleProcessData} data={props.handleManipulation(props.children.props.data)}  /> as TabElement<T>;
    array.push(mChild)
  }
    
  return <div>{array.map((item) => (item))}</div>;
}


export type Data = { hello: string };

export function App() {

  //B.C. it's generic you going to have to have some form of generic control functions
  const handleProcessData = (data: Data) => {
    //Here you have to specifiy how this specific data type is processed
    return data.hello;
  };

  const handleManipulation = (data: Data) => {
    //here you would have all your manipulation logic
    return { hello: data.hello + " is manipulated" };
  }

 //To Make this easier to use you could nest handleProcessData inside the Tabs component
  return (
    <div>
      <Tabs<Data> handleManipulation={handleManipulation}>
        <Tab<Data> handleProcessData={handleProcessData} data={{hello: "world1"}} />
        <Tab<Data> handleProcessData={handleProcessData} data={{hello: "world2"}} />
      </Tabs>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for taking the time to come up with this solution. Definitely picked up a few tricks. I'm sorry if it was not so clear what I want to achieve, but in reality I'll both be splitting up the children array into a visible and overflow part, but I'll also render the label differently based on what is in the data prop (if any). So it's not so much about mutating the data property, but more like making changes to the children based on what is in the data object for the given child / tab element. The problem is that when trying to achieve this with render props, data loses its type

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.