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.