100

I'm trying to take advantage of the recently added support for typing of children in the TypeScript compiler and @types/react, but struggling. I'm using TypeScript version 2.3.4.

Say I have code like this:

interface TabbedViewProps {children?: Tab[]}
export class TabbedView extends React.Component<TabbedViewProps, undefined> {

  render(): JSX.Element {
    return <div>TabbedView</div>;
  }
}

interface TabProps {name: string}
export class Tab extends React.Component<TabProps, undefined> {
  render(): JSX.Element {
    return <div>Tab</div>
  }
}

When I try to use these components like so:

return <TabbedView>
  <Tab name="Creatures">
    <div>Creatures!</div>
  </Tab>
  <Tab name="Combat">
    <div>Combat!</div>
  </Tab>
</TabbedView>;

I get an error as follows:

ERROR in ./src/typescript/PlayerView.tsx
(27,12): error TS2322: Type '{ children: Element[]; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<TabbedView> & Readonly<{ children?: ReactNode; }> ...'.
  Type '{ children: Element[]; }' is not assignable to type 'Readonly<TabbedViewProps>'.
    Types of property 'children' are incompatible.
      Type 'Element[]' is not assignable to type 'Tab[] | undefined'.
        Type 'Element[]' is not assignable to type 'Tab[]'.
          Type 'Element' is not assignable to type 'Tab'.
            Property 'render' is missing in type 'Element'.

It seems to be inferring the type of children as just Element[] instead of Tab[] even though that's the only type of children I'm using.

EDIT: It would also be fine to restrict the interface of the children props instead of restricting the type of the children components directly, since all I need to do is pull some specific props out of the children components.

Christopher Armstrong
  • 2,107
  • 2
  • 14
  • 22

5 Answers5

58

Edit 2: Turns out that this approach prevent the warning, but according to the comments TabProps aren't properly checked.

You should try to set children of interface TabbedViewProps like so

interface TabbedViewProps { children?: React.ReactElement<TabProps>[] }

The idea here is not to tell your TabbedView has an array of Tab, but instead tell your TabbedView he has an array of element which takes specific props. In your case TabProps.

Edit ( thx to Matei ):

interface TabbedViewProps {
    children?: React.ReactElement<TabProps>[] | React.ReactElement<TabProps>
}
Pierre Ferry
  • 1,329
  • 12
  • 18
  • 5
    I follow a similar approach: `children?: Array | React.ReactChild`. This way the component can accept even a single child and not necessarily an array of children. – Matei Radu Mar 21 '18 at 14:30
  • 33
    I have a similar problem. In my case, I defined type of children(which is TabProps) just like you did, but when I place an element as TabbedView's child which does not shaped like TabProps, typescript does not report any error or warning. Can you give some advice or clue? – zmou-d Oct 18 '18 at 07:03
  • 4
    Same issue as @Noah. It seems that you can restrict children only to an extent, but restricting to child elements *with certain props* just doesn't work. It'll make sure its an element, but props don't matter. Bummer! – Aaron Beall Jan 23 '19 at 02:59
  • 1
    Does Typescript plan to properly type props of a child element? – Ben Carp Apr 13 '19 at 11:03
  • 4
    I’m facing the same problem. TypeScript does not check the type of children even though I define the `children` type like above. – Jess Jun 28 '19 at 02:48
  • 7
    It looks like wrapping the props with ReactElement loosens the type so that any element is actually allowed. Here is what I get when hovering over my customer element: `type ChartElement = ReactElement, string | ((props: any) => ReactElement Component)> | null) | (new (props: any) => Component)>` Note the `|(props: any) =>`. – fantapop Jun 28 '19 at 20:11
  • 1
    @zmou-d running into this same problem. Did you already come to a fix for this issue? – MapMyMind Jan 08 '21 at 13:08
  • @MapMyMind I have got the same issue, did you find a solution? – Sebastian Nielsen May 27 '22 at 11:32
27

As pointer out already, declaring TabbedView.children as:

children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[];

Will get rid of the error, but it won't be type-checking the children properly. That is, you will still be able to pass children other than TabProps to TabbedView without getting any error, so this would also be valid:

return (
  <TabbedView>
    <Tab name="Creatures">
      <div>Creatures!</div>
    </Tab>

    <Tab name="Combat">
        <div>Combat!</div>
    </Tab>

    <NotTabButValidToo />
  </TabbedView>
);

What you could do instead is declare a prop, let's say tabs: TabProps[], to pass down the props you need to create those Tabs, rather than their JSX, and render them inside TabbedView:

interface TabbedViewProps {
  children?: never;
  tabs?: TabProps[];
}

...

const TabbedView: React.FC<TabbedViewProps> = ({ tabs }) => {
  return (
    ...

    { tabs.map(tab => <Tab key={ ... } { ...tab } />) }

    ...
  );
};
Danziger
  • 19,628
  • 4
  • 53
  • 83
  • Been researching this for a while and I think this is the best method given the children limitations – Nicholas Haley Nov 24 '20 at 18:23
  • 19
    Opting for a worse API just because type checker limitations doesn't seem like a smart decision to me. – Danielo515 Apr 22 '21 at 07:32
  • 2
    IMO It's not a worse api, but a better one. Just because it's maybe not so easy to type doesn't mean it is objectively "worse". This api more clearly expresses what is wanted. We want a list of tabs (which is what the typings clearly say). Now their is no `React.Children` nonsense as well. This approach really should be more idiomatic – Zachiah Dec 24 '22 at 20:18
6

I tried to assert the type. You can throw or just ignore.

interface TabbedViewProps {
  children?: React.ReactElement<ITabProps> | React.ReactElement<ITabProps>[]
}

And in the component itself map the children and assert or ignore

{React.Children.map(props.children, (tab) => {
  if(tab?.type != Tab) return;
  console.log(tab?.type == Tab);
  return tab;
})}
Maor Dahan
  • 356
  • 4
  • 8
1

These answers show the general idea, but they don't allow you to pass children like:

<MyParentComponent>
  {condition && <Child1/>}
  {list.map((it) => <Child2 x={it}/>}
</MyParentComponent>

I took some inspiration from the definition of children in type PropsWithChildren<P> from the React (v16.14.21) codebase:

type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

and came up with a simplified definition that fits my use case:

type TypedReactNode<T> = ReactElement<T> | Array<TypedReactNode<T>> | null | undefined;
type PropsWithTypedChildren<P, C> = P & { children?: TypedReactNode<C> | undefined };

Finally, I can define my component like so:

type MyParentComponentProps = {
  whatever: string;
};

const MyParentComponent = (props: PropsWithTypedChildren<MyParentComponentProps, AllowedChildType>) => {
  // body
}
cbreezier
  • 1,188
  • 1
  • 9
  • 17
-3

Type you are returning in Tab render method is JSX.Element. This is what causes your problem. TabbedView is expecting array of childrens with type Tab. I am not sure if you can specify a certain component as a children type. It can be string or JSX.Element. Can you show the definition file for Tab?

Look at https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts to see how JSX.Element interface looks.

Moonjsit
  • 630
  • 3
  • 11
  • I showed the entire definition of TabbedView and Tab in the first code block. The whole point of the new support for children types *seems* to be to allow restricting the components (if you see the issue that it's supposed to resolve linked at the top of the PR), so I think I must be missing something. – Christopher Armstrong Jun 10 '17 at 17:16
  • You have showed a definition of the component. Not interface. – Moonjsit Jun 10 '17 at 17:18
  • I'm not really sure what you're asking for, I'm sorry :( This is the entirety of my Tab-related code in my tsx project. I would be happy to try to provide what you're asking for but I think I'm going to need a little bit of help. – Christopher Armstrong Jun 10 '17 at 17:22
  • edited: still not working. I tried React.ReactElement, which seemed to work, but didn't properly reject non-Tab components in the children. – Christopher Armstrong Jun 10 '17 at 17:40
  • You are using instance of react component as a type (interface). https://www.typescriptlang.org/docs/handbook/interfaces.html – Moonjsit Jun 10 '17 at 22:04
  • I don't understand. Can you point out exactly where I'm using an instance of a React component as a type? – Christopher Armstrong Jun 11 '17 at 02:28