Detecting React children array length in Typescript - reactjs

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?

Related

Type 'Element[]' is missing the following properties when handling react children

I have a parent component that provides its children to a child component. The children can either be a node or an array of nodes, and depending on if it is a node or an array of nodes the children in the ChildComponent are rendered differently. However, when I a render the ChildComponent in the ParentComponent, I get the following Typescript error:
TS2786: 'ChildComponent' cannot be used as a JSX component.   Its return type 'Element | Element[]' is not a valid JSX element.     Type 'Element[]' is missing the following properties from type 'Element': type, props, key
Please see the below for the code. How can I render the ChildComponent without any Typescript errors? Thanks!
import React from 'react';
import styles from './component.scss';
interface Props {
children: React.ReactChildren;
}
const ChildComponent = ({ children }: Props): JSX.Element | JSX.Element[] => {
if (React.Children.count(children)) {
return React.Children.map(children, (child) => (
<div className={styles.childContainer}>{child}</div>
));
}
return <div className={styles.singleContainer}>{children}</div>;
};
const ParentComponent: React.FC<Props> = ({ children }) => (
<div className={styles.container}>
<ChildComponent>{children}</ChildComponent>
</div>
);
export default ParentComponent;
In your current code, the return type is JSX.Element[] from if block (incorrect) but JSX.Element (correct) from else part. That's the reason of the error you are seeing.
To fix it, you need to return single JSX expression from ChildComponent as "JSX expressions must have one parent element".
You can use a parent element like div, span or simply use React.Fragment (shorthand <></>):
const ChildComponent = ({ children }: Props) => {
if (React.Children.count(children)) {
return (
<> // HERE
{React.Children.map(children, (child) => (
<div className={styles.childContainer}>{child}</div>
))}
</>
);
}
return <div className={styles.singleContainer}>{children}</div>;
};
And children type should be ReactNode instead of ReactChildren:
interface Props {
children: React.ReactNode;
}
As these are the definitions of both the typings:
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
interface ReactChildren {
map<T, C>(children: C | C[], fn: (child: C, index: number) => T):
C extends null | undefined ? C : Array<Exclude<T, boolean | null | undefined>>;
forEach<C>(children: C | C[], fn: (child: C, index: number) => void): void;
count(children: any): number;
only<C>(children: C): C extends any[] ? never : C;
toArray(children: ReactNode | ReactNode[]): Array<Exclude<ReactNode, boolean | null | undefined>>;
}
Also, I would suggest not to use React.FC.
Here is a demo.
Edit:
PS: The else block <div className={styles.singleContainer}>{children}</div> and the if condition with count is not really required.

How can I constraint a type if use element ref as an input value of a function

I have tried codes like this, but it seems T should have some constraints which I am not clear.
export const useResizeObserver = <T>(elementRef: ElementRef<T>, debounceTimeout = 100) => {}
These are the restraints for what T you can pass to an ElementRef, from the React typings:
T extends
| ForwardRefExoticComponent<any>
| { new (props: any): Component<any> }
| ((props: any, context?: any) => ReactElement | null)
| keyof JSX.IntrinsicElements
It can be any ComponentType (FunctionComponent or ClassComponent), a built-in DOM element like div, and some other complicated things.

What type to assign to children when they are all react components

I need to access children's props in react using typescript. However, when I try to do it, I receive the following error:
Property 'props' does not exist on type 'string | number | boolean | {} | ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<...>)> | ReactNodeArray | ReactPortal'.
Property 'props' does not exist on type 'string'.
My Props
type MyProps = {
title: string,
children: React.ReactNode[],
}
React.Node should be used when we don't know if children are a simple string, a number, a React element, etc.
When we want to have only React Component as possible children we have to use React.ReactElement instead. Furthermore, with react element, we can also specify the type of our children props by doing React.ReactElement<MyProps> or React.ReactElement<MyProps>[] in the specif case of an array.
So modify your props in:
type MyProps = {
title: string,
children: React.ReactElement<ChildrenProps>[],
}

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

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

What type is best to pass one React Component to another

If I want to pass some array of React Components to a React Component, I thought I could use the type Component: React.ComponentClass<any> | React.StatelessComponent<any> in props (as per this issue), for example:
interface Props {
elements: (React.ComponentClass<any> | React.StatelessComponent<any>)[];
}
const App = (props: Props) => (
<>
{props.elements.map(X => <X />)}
</>
)
But when following this pattern, I get the following error:
Element type 'X' does not have any construct or call signatures
Ok, sure; it needs to be a function, so maybe:
interface Props {
elements: (() => (React.ComponentClass<any> | React.StatelessComponent<any>))[];
}
But then I am told that this type is missing properties type, key, and props.
My question is, what type should I use in this situation?
Going with the type
(() => React.ReactElement)[]
Seems to remove any errors, but is this the best way to do this?
Actually an even better type is React.ReactNode, that is:
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
(React.ReactElement is part of ReactChild).
Basically React.ReactNode is anything that can be rendered inside jsx:
const TestApp: React.FunctionComponent<{ elements: Array<React.ReactNode> }> = props => {
return <>{...props.elements}</>;
};
You should also look at reacts child props. That can be used to compose elements like writing plain HTML:
<Test>
<Component1 />
<Component1 />
</Test>
And your test component would be:
const TestApp: React.FunctionComponent = props => {
return <>{...props.children}</>;
};

Resources