Dac0d3r
Dac0d3r

Reputation: 1854

How to use TypeScript generics with React render props

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.

Upvotes: 0

Views: 2321

Answers (1)

Nicholas_Jones
Nicholas_Jones

Reputation: 330

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"));

Upvotes: 1

Related Questions