Skip to content

Suggestion: type checking for JSX children #13618

Closed
@jwbay

Description

@jwbay

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: JSX/TSXRelates to the JSX parser and emitterFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions