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.

Activity

joelday

joelday commented on Jan 30, 2017

@joelday
Contributor

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

    // 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'.```
jwbay

jwbay commented on Feb 2, 2017

@jwbay
ContributorAuthor

@joelday Oh, nice! Many thanks for taking at look at an implementation!

added this to the TypeScript 2.3 milestone on Feb 2, 2017
joelday

joelday commented on Feb 9, 2017

@joelday
Contributor

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

33 remaining items

donaldpipowitch

donaldpipowitch commented on Aug 8, 2017

@donaldpipowitch
Contributor

How could this happen?

yuit was unassigned by donaldpipowitch 18 minutes ago

antanas-arvasevicius

antanas-arvasevicius commented on Aug 8, 2017

@antanas-arvasevicius
RyanCavanaugh

RyanCavanaugh commented on Aug 8, 2017

@RyanCavanaugh
Member

@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

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.

donaldpipowitch

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.

patsissons

patsissons commented on Aug 31, 2017

@patsissons

what has become of this issue, is it dead?

donaldpipowitch

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

gitsupersmecher

gitsupersmecher commented on Oct 26, 2017

@gitsupersmecher

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

dontsave

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

luixo

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.

jchitel

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.

mhegazy

mhegazy commented on Feb 13, 2018

@mhegazy
Contributor

#21699 should cover this scenario.

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

        Participants

        @Jessidhia@joscha@joelday@cchamberlain@yuit

        Issue actions

          Suggestion: type checking for JSX children · Issue #13618 · microsoft/TypeScript