Skip to content

Suggestion: type checking for JSX children #13618

Closed

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.

Activity

  1. joelday commented on Jan 30, 2017

    @joelday
    Contributor

    jwbay Ryan Cavanaugh (@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.

        // A custom element type with an explicit children prop attribute type
        
        class MyParentClass extends React.Component<{
          children?: MyClass[];
        }, any> {
        }
        
        // OK - Child element matches the children prop
        var d1 = <MyParentClass><MyClass reqd={true} /><MyClass reqd={true}></MyClass></MyParentClass>
        // Error - Incorrect child element type
        var d2 = <MyParentClass><div /><div></div><MyClass reqd={true} /><MyClass reqd={true}></MyClass></MyParentClass>
                                ~~~~~~~
    !!! error TS2322: Type 'HTMLProps<HTMLDivElement>' is not assignable to type 'MyClass'.
    !!! error TS2322:   Property 'setState' is missing in type 'HTMLProps<HTMLDivElement>'.
                                       ~~~~~
    !!! error TS2322: Type 'HTMLProps<HTMLDivElement>' is not assignable to type 'MyClass'.```
    
  2. jwbay commented on Feb 2, 2017

    @jwbay
    ContributorAuthor

    Joel Day (@joelday) Oh, nice! Many thanks for taking at look at an implementation!

  3. joelday commented on Feb 9, 2017

    @joelday
    Contributor

    Yui (@yuit) Mohamed Hegazy (@mhegazy) Let me know if there's anything I can do to help with this. Thank you!

  4. joelday commented on Mar 29, 2017

    @joelday
    Contributor

    Yui (@yuit) Any traction with this? Would love for this to still make it into 2.3. Please let me know if there's anything I can do.

  5. petdun2519 commented on Apr 3, 2017

    @petdun2519

    Would also like to see this implemented!

  6. yuit commented on Apr 6, 2017

    @yuit
    Contributor

    Joel Day (@joelday) petdun2519 PR will go up soon. Thanks for your protoype

  7. 15 remaining items

  8. donaldpipowitch commented on Aug 8, 2017

    @donaldpipowitch
    Contributor

    The more I think of it I come to the conclusion that this could probably not be solved at a type checking level, because of the way TSX works currently. We can't use something like tagging JSX.Element:

    enum FooterTag {}
    type FooterElement = JSX.Element & FooterTag;
    const Footer = () => <div>Footer.</div> as FooterElement;
    
    interface ModalProps {
      children: FooterElement;
    }
    const Modal = ({ children }: ModalProps) =>
      <div>
        {children}
      </div>;
    
    // `<Footer />` *needs* to be casted to `FooterElement`,
    // because the return value is always `JSX.Element`
    const UseModal = () =>
      <Modal>
        {<Footer /> as FooterElement}
      </Modal>;

    When we useTSX the return value of our component will always be JSX.Element, because of the way it is transpiled. If we can't change this way, than the only solution would be to make this check inside a linter rule. With a linter rule we could easily say "Hey, you can only use <Footer /> inside <Modal />." or something like that. This would be possible, if #15589 could be integrated into the language.

  9. donaldpipowitch commented on Aug 8, 2017

    @donaldpipowitch
    Contributor

    How could this happen?

    yuit was unassigned by donaldpipowitch 18 minutes ago

  10. antanas-arvasevicius commented on Aug 8, 2017

    @antanas-arvasevicius
  11. RyanCavanaugh commented on Aug 8, 2017

    @RyanCavanaugh
    Member

    Donald Pipowitch (@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

  12. jwbay commented on Aug 8, 2017

    @jwbay
    ContributorAuthor

    Couldn't JSX.Element be made generic with props/attributes?

    const i = <input value='42' />;
    //yields JSX.Element<{ value: string }>
    
    const c = <Component prop={ 42 } />
    //yields JSX.Element<{ prop: number }>

    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.

  13. donaldpipowitch commented on Aug 9, 2017

    @donaldpipowitch
    Contributor

    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

    Ah, I see. Thanks. So if you ever need someone new (remote)... 😏

    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.

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

    The <colgroup> must appear after any optional <caption> element but before any <thead>, <th>, <tbody>, <tfoot> and <tr> element.

  14. patsissons commented on Aug 31, 2017

    @patsissons

    what has become of this issue, is it dead?

  15. donaldpipowitch commented on Sep 7, 2017

    @donaldpipowitch
    Contributor

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

  16. gitsupersmecher commented on Oct 26, 2017

    @gitsupersmecher

    something like this is a mitigation: export interface IMenuPorps { children ? : any; }

  17. dontsave commented on Jan 6, 2018

    @dontsave

    Curious about the status of this issue as well. Being able to restrict children to specific types would be very useful

  18. luixo commented on Feb 1, 2018

    @luixo

    Chances are I misunderstood the issue topic, but I have somehow related problem.
    So I have some component with its props typed:

    export interface MyProps {
        extraElement: React.ReactNode;
    }
    
    class MyInnerComponent extends React.Component<MyProps> {
        render(): React.ReactNode {
            return (
                <div>
                    {this.props.extraElement}
                    {this.props.children}
                </div>
            );
        }
    }
    
    export default MyInnerComponent;

    Now I want to calculate children in another component.

    import MyInnerComponent, {MyProps as InnerComponentProps} from '...';
    
    class MyWrapperComponent extends React.Component<{}> {
        calculateProps(): InnerComponentProps {
            return {
                extraElement: <div>Hello!</div>,
                children: [<div>First</div>, <div>Second</div>]
            };
        }
        render(): React.ReactNode {
            return (
                <MyInnerComponent {...this.calculateProps()} />
            );
        }
    }

    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 for children being React.ReactNode or something, but I didn't found anything that look like that in @types/react.

  19. jchitel commented on Feb 13, 2018

    @jchitel

    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.

  20. mhegazy commented on Feb 13, 2018

    @mhegazy
    Contributor

    #21699 should cover this scenario.

  21. locked and limited conversation to collaborators on Jul 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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