Description
Rationale
Props are an important part of a React component's interface, but children can be as well.
In applications it's not uncommon to want to restrict what kind of children a component will
accept. Currently this enforcement has to be done at runtime by throwing errors.
The Goal
Components can specify allowed children as a union type. Each member would correspond to
the tag, or 'type' field, of the resulting Element. Ideally this could be plucked from
the type of props.children, which would satisfy both component classes and SFCs.
Motivating Examples:
A modal component may want to make assumptions about its children to satisfy layout
constraints.
interface IModalProps {
children: ModalHeader | ModalBody | ModalFooter;
}
function Modal({ children }: IModalProps) { ... }
function ModalHeader(props) { ... }
function ModalBody(props) { ... }
function ModalFooter(props) { ... }
<Modal>
<ModalHeader />
<div /> { /* Desired error: type 'div' does not exist in type 'ModalHeader | ModalBody | ModalFooter' */ }
<ModalBody />
<ModalFooter />
</Modal>
Similarly...
interface IButtonGroupProps {
children: 'button';
}
function ButtonGroup({ children }: IButtonGroupProps) { ... }
<ButtonGroup>
<button />
<button />
<a href='' /> { /* Desired error: type 'a' is not assignable to type 'button' */ }
</ButtonGroup>
An interesting emerging pattern is using JSX as a function call to turn imperative APIs into
declarative ones. Currently these 'render callbacks' can't be type-checked at all. A more complete
summary can be found at http://reactpatterns.com/#render-callback. A further example can be found at https://github.com/ReactTraining/react-media#usage.
interface IUser {
Name: string;
}
interface IFetchUserProps {
children(user: IUser): any;
}
class FetchUser extends React.Component<IFetchUserProps, any> {
render() {
return this.state
? this.props.children(this.state.result)
: null;
}
componentDidMount() {
api.fetchUser().then(result => this.setState({ result }));
}
}
function UserName() {
return (
<FetchUser>
{ user => (
<h1>{ user.NAme }</h1> // Desired error: property 'NAme' does not exist on type 'IUser'
) }
</FetchUser>
);
}
Lastly, I don't think any of this is necessarily specific to React. Other libraries leveraging the JSX
specification for component structure should be able to leverage this as well. The crux of this lies at
the relationship between the type of the 'children' attribute of a given tag and the children actually
passed to it at usage sites.
Activity
joelday commentedon Jan 30, 2017
@jwbay @RyanCavanaugh Did a quick prototype of this: joelday@50870c1
I'm sure there are a bunch of little gotchas here, but it's a start.
jwbay commentedon Feb 2, 2017
@joelday Oh, nice! Many thanks for taking at look at an implementation!
joelday commentedon Feb 9, 2017
@yuit @mhegazy Let me know if there's anything I can do to help with this. Thank you!
33 remaining items
donaldpipowitch commentedon Aug 8, 2017
How could this happen?
antanas-arvasevicius commentedon Aug 8, 2017
RyanCavanaugh commentedon Aug 8, 2017
@donaldpipowitch Yui's left the team but was still assigned the issue. GitHub fixes this invalid assignment whenever someone makes any change (incl. comment) on an issue
jwbay commentedon Aug 8, 2017
Couldn't JSX.Element be made generic with props/attributes?
At this point the restricted children case devolves slightly from 'you can only pass something shaped like ModalFooter here' to 'you can only pass something with props shaped like ModalFooter's props', but for components I think that's really all that matters.
donaldpipowitch commentedon Aug 9, 2017
Ah, I see. Thanks. So if you ever need someone new (remote)... 😏
As far as I understand this issue and what I personally would like to see is the first case ("you can only pass something shaped like ModalFooter here"), not the second case ("you can only pass something with props shaped like ModalFooter's props").
I want to enforce ordering "rules" of components similar to native DOM element ordering "rules". E.g. from
<colgroup>
:patsissons commentedon Aug 31, 2017
what has become of this issue, is it dead?
donaldpipowitch commentedon Sep 7, 2017
I don't want to generate noise for the maintainers, but maybe we should just open a new issue for a focussed discussion and re-evaluation of this feature...?
gitsupersmecher commentedon Oct 26, 2017
something like this is a mitigation: export interface IMenuPorps { children ? : any; }
dontsave commentedon Jan 6, 2018
Curious about the status of this issue as well. Being able to restrict children to specific types would be very useful
luixo commentedon Feb 1, 2018
Chances are I misunderstood the issue topic, but I have somehow related problem.
So I have some component with its props typed:
Now I want to calculate children in another component.
In this I get
TS2322: Type { extraElement: Element; children: Element[] } is not assignable to type 'MyProps'. Object literal may only specify known properties, and 'children' does not exist in type 'MyProps'
.Probably, I should extend
MyProps
with something that includes a proper check forchildren
beingReact.ReactNode
or something, but I didn't found anything that look like that in@types/react
.jchitel commentedon Feb 13, 2018
Recent discussion in this issue seems to be about allowing the JSX.Element type to be generic, or at least having JSX typing be more flexible. These use cases are covered by this issue, which is still open. I'd suggest moving all conversation there so that we can move toward an implementable proposal.
mhegazy commentedon Feb 13, 2018
#21699 should cover this scenario.