Why React.FC doesn't allow me to just return children? - reactjs

While trying to create a component I realized a situation.
Only returning children
interface FooProps {
children?: React.ReactNode
}
const Foo: React.FC<FooProps> = ({ children }) => {
return children
}
Will give me an error saying:
Type '({ children }: PropsWithChildren<FooProps>) => ReactNode' is not assignable to type 'FC<FooProps>'.
Type 'ReactNode' is not assignable to type 'ReactElement<any, any> | null'.
Type 'undefined' is not assignable to type 'ReactElement<any, any> | null'.
But if I return children inside any jsx, or even Fragment:
const Foo: React.FC<FooProps> = ({ children }) => {
return <>{children}</>
}
It won't give me any error.
The obvious answer to this is that the types are incompatible, ReactNode is not assignable to type ReactElement<any, any> | null, as the error says, but my question is Why?
Why returning a ReactNode (e.g. children) isn't allowed? Shouldn't it be allowed?
Something extra to ask would be if this is something about React.FC and probably other type will be ok if I return ReactNode or if this is with all react components?

Because React.ReactNode type is a union type. Let's see what the type definition for it looks like,
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
Expected return type when you create a functional component with generic FC
ReactElement<any, any> | null
So when you return the children directly from the component the compiler complains because the types are simply not compatible. The reason being ReactElement is a valid React.ReactNode but not the other way around, because React.ReactNode can also be a value of type ReactFragment or ReactPortal and so on.
It is obvious that it will create a type mismatch with ReactElement. But when you return the children inside a Fragment the compiler no longer complains because the return type becomes valid. Take a look at this example without the FC generic,
// The return type of Foo is inferred as React.ReactNode
// Compiler doesn't complain because we don't annotate the return type
const Foo = ({ children }: PropsWithChildren<FooProps>) => {
return children;
};
// But when you use React.FC generic it is the same as annotating the return type
// of Foo as `React.ReactElement`
// compiler will complain because of the type mismatch
const Foo = ({ children }: PropsWithChildren<FooProps>): React.ReactElement => {
return children;
};
Example for why it works when you return the children inside a Fragment
// Without generic FC
// Return type infered as JSX.Element which simply extends React.ReactElement
const Foo = ({ children }: PropsWithChildren<FooProps>) => {
return <>children</>;
};
// With generic FC
// Compiler doesn't complain because of JSX.Element can be valid-
// return type because it simply extends the interface React.ReactElement
const Foo: FC<FooProps> = ({ children }) => {
return <>children</>;
};
Edit - This may not be a direct answer, but I hope this example can explain more on why React.ReactNode should not be allowed.
React.ReactNode is broad, and you can assign almost anything to it. For example
const Foo = () => { return 45; };
// no compile time error because even a functions are a valid ReactNode
const Bar: React.ReactNode = Foo
But what happens when we try to render it inside any valid component
At compile time - No error
const FooBar: React.FC<FooProps> = () => {
// again no compile time error because Bar is a valid ReactNode
return <div>{Bar}</div>
}
At runtime - Error Functions are not valid react child

Related

Forcing children type in react-typescript: Type 'Element' is not assignable to type 'FunctionComponent<T> [duplicate]

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.
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>
}
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 } />) }
...
);
};
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;
})}
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
}
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.

Detecting React children array length in Typescript

I have a component that can accept React children as either a node or an array of nodes. I want to be able to detect if children is an array of nodes, but I am getting the following Typescript error:
TS2339: Property 'length' does not exist on type 'string | number | true | {} | ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)> | ... 47 more ... | (ReactNode[] & ReactPortal)'.   Property 'length' does not exist on type 'number'.
Is there a way in Typescript where I detect if children has length? Thanks.
import React from 'react';
interface Props {
children: React.ReactNode | React.ReactNode[];
}
const SampleComponent: React.FC<Props> = ({ children }) => {
if (children && children.length) {
return children.map((child) => (
<div>{child}</div>
));
}
return children;
};
export default SampleComponent;
There is a React.Children API for dealing with the props.children (which is an opaque data structure).
Also, React.Children has methods like count, map etc.
if (React.Children.count(children)) {
return React.Children.map(children, (child) => <div>{ ..todo.. }</div>)
}
It has TypeScript support too. So, you won't need to cast the types.
[Update]
Please check out Ajeet Shah's response with using React.Children API.
[Original]
Cast children to React.ReactNode[] before checking length like this:
(children as React.ReactNode[]).length
You could also create a type predicate:
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
Try React.Children.count(children)
The best way in this case is not use Reacf.FC
Defined the props like const MyButton = (props: MyProps)=>...
Or use React.VFC
Function Components - Why is React.FC discouraged?

Which would be the Typescript Types for React Components that can return a string or directly their children

Which would be the correct types for a React Component that might also return a string or directly their children, apart from a JSX.Element? for example:
type PropsStringExample = Readonly<{
returnString: boolean;
}>;
type PropsChildrenExample = Readonly<{
children: React.ReactNode;
returnChildren: boolean;
}>;
/*
Fails with: Type '{} | null | undefined' is not assignable to type 'ReactElement<any, any> | null'.
Type 'undefined' is not assignable to type 'ReactElement<any, any> | null'.
*/
const ComponentReturnsChildren: React.FunctionComponent<PropsChildrenExample> = ({
children,
returnChildren
}: PropsChildrenExample) => {
if (returnChildren) {
return children;
}
return (
<>
<div>Just a wrapper around the children</div>
{children}
</>
);
};
/*
Fails with: Type '"This component returns a string"' is not assignable to type 'ReactElement<any, any> | null'.
*/
const ComponentReturnsString: React.FunctionComponent<PropsStringExample> = ({
returnString
}: PropsStringExample) => {
if (returnString) {
return "This component returns a string";
}
return <div>Or also some dumb div</div>;
};
Wrapping the returned string with a fragment on the last example is not possible either due to some eslint contraints i.e <>"string"</>
CodeSandbox with an example of the errors: https://codesandbox.io/s/type-component-return-children-or-string-b956g?file=/src/index.tsx
A React.FunctionComponent must return a JSX element. A string doesn't match that description and would have to be wrapped in JSX in order to be valid. You could change the type from React.FunctionComponent to JSX.element | string (or something similar), but then in your consuming component you'd have to wrap that in a JSX.element conditionally or use a JSX element that makes sense to contain either a bare string or a JSX.Element. I might suggest that you rethink your approach here, or use a <span> instead of a <> fragment.

What is proper type for React's children as function?

I'm passing a children as a function, returning React.ReactNode into Provider HOC like follows:
<Provider variables={{ id: "qwerty" }}>
{data => (
<ExampleComponent data={data} />
)}
</Provider>
Here's a provider component example code:
type Props = {
id: string;
children: any; // What type should be put here?
};
const Provider: React.SFC<Props> = ({ id, children }) => {
const { data, loading, error } = useQuery<req, reqVariables>(query, {
variables: { id }
});
if (loading) return "Loading...";
if (error) return "Error";
if (!data) return "No data";
return children(data);
}
If I'm not specifying any type for children, so leaving it to default to React.ReactNode, then line return children(data); shows an error:
Cannot invoke an expression whose type lacks a call signature. Type 'string | number | boolean | {} | ReactElement ReactElement Component)> | null) | (new (props: any) => Component)> | ReactNodeArray | ReactPortal' has no compatible call signatures.
If I'm specifying children explicitly in type Props as any, everything works great, but it's Wrong from a Typescript and types perspective.
What is a proper type to be put for props.children, when a function returning React.ReactNode is passed instead of React.ReactNode itself, so Typescript would accept it as a valid "Render function" and will allow calling children like function?
You can specify the children type to be a function which returns a ReactNode
type Props = {
id: string,
children: (props: InjectedCounterProps) => React.ReactNode
};
JSX.Element can also be the type, in case the children being 'DOM element'.

What is the TypeScript return type of a React stateless component?

What would the return type be here?
const Foo
: () => // ???
= () => (
<div>
Foobar
</div>
)
StatelessComponent type mentioned in this answer has been deprecated because after introducing the Hooks API they are not always stateless.
A function component is of type React.FunctionComponent and it has an alias React.FC to keep things nice and short.
It has one required property, a function, which will return a ReactElement or null. It has a few optional properties, such as propTypes, contextTypes, defaultProps and displayName.
Here's an example:
const MyFunctionComponent: React.FC = (): ReactElement => {
return <div>Hello, I am a function component</div>
}
And here are the types from #types/react 16.8.24:
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
interface ISomeCoolInterface {
some: 'string';
cool: 'string';
props: 'string'
}
const SomeCoolComponent
: React.FC<ISomeCoolInterface>
= ({ some, cool, props }): JSX.Element => {
return <SomeCoolComponent>{some, cool, props}</SomeCoolComponent>
}
The important bit here being the return type JSX.Element
The correct return type here is ReactElement<P>, but a better option would be to use React.StatelessComponent<P> like this
const Foo
: React.StatelessComponent<{}>
= () => (
<div>
Foobar
</div>
)
If using the function keyword, the best return type appears to be JSX.Element | null.
For now our team is using JSXNode as shorthand, since these are the only two types that can be returned directly as a JSX result:
type JSXNode = JSX.Element | null;
Edit: looks like eventually React.ReactNode is the intended return type for JSX but it's currently not possible. (Reference)
Background:
None of the answers here seem to address the most common modern case - that you have a function returning an element. What type should this return?
function MyComponent(): SomeTypeHere {
return <>...</>;
}
The recommended way to hide the component is to return null, so it's not clear what clean return type that would be. Typing JSX.Element | null everywhere or even making a custom type like that seems it should be unnecessary given how universal this case is. ReactNode also doesn't work because undefined can't be returned as JSX.
Overall the best return type seems to be JSX.Element | null. That is the return type of the FC type which is used if you're not using the function keyword:
const MyComponent: FC = () => { <>...</> }
I would also add .SFC, which stands for Stateless Functional Component.
const Foo
: React.SFC<{}>
= () => (
<div>
Foobar
</div>
)
See https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts
Each JSX element is just syntactic sugar for calling React.createElement(component, props, ...children).
function createElement<P extends DOMAttributes<T>, T extends Element>(
type: string,
props?: ClassAttributes<T> & P,
...children: ReactNode[]): DOMElement<P, T>;
So it's DOMElement<P, T>

Resources