如何使用 TypeScript 2.3中新添加的支持来限制 TypeScript 中 React Children 的类型?

我正在尝试利用 TypeScript 编译器中的 最近增加了对儿童输入的支持和@type/response,但是很困难。我使用的是 TypeScript 版本2.3.4。

假设我有这样的代码:

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>
}
}

当我像这样使用这些组件时:

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

我得到如下错误:

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'.

它似乎推断的孩子的类型只是 Element[]而不是 Tab[],即使这是我使用的唯一类型的孩子。

编辑: 也可以限制 儿童道具的接口,而不是直接限制子组件的类型,因为我需要做的只是从子组件中提取一些特定的道具。

51352 次浏览

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.

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
}