eslint-plugin-react: [prop-types] Props validation with TS StatelessComponent (SFC) type fails

A typical pattern for parent components fails with the error, 'children' is missing in props validation react/prop-types.

import React, { StatelessComponent } from 'react'

const Wrapper: StatelessComponent = ({ children }) => (
  <div style={{ margin: '1rem' }}>{children}</div>
)

This looks okay to me. Presumably, the resulting JavaScript doesn’t produce the validation eslint-plugin-react expects. Any ideas what’s going on, and whether it’s a bug or how it can be avoided?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 19 (8 by maintainers)

Most upvoted comments

No, you don’t. Types are at build-time only; propTypes run at runtime.

Separately, altho not in this or every case, propTypes are much more powerful and expressive than types. You should be using both TS types and propTypes.

(Separately, the type for “children” in a stateless component is “any”, not “node” - only you (and not the type) can know you’re passing the children prop as a child of a div, thus requiring it to be a node.

If you prefer to sacrifice a major benefit in exchange for your type system, then you’ll need to disable the prop-types rule, whose purpose is to force you to use propTypes.

Gotcha! Well, that adds up - today I learned 💯

@LordZardeck TypeScript decides this, not me or JS - in your class, you do <Props> directly inside the class construct, which means that the named class is typed without needing inference. However, when you use an arrow function - as opposed to a normal, proper, named function - you’ve attached the type to the variable and not to the function itself, and the TS eslint parser seems not to pass the type info to us when you do that.

This is just another in a long list of reasons to not use arrow functions for functional components.

I got here from Google, so on the odd chance my findings and conclusions will help someone, I’ll document them here.

If you’re using exclusively react-typescript with strict type checking (no js mixed in), then in my opinion it is safe to disable the react/prop-types eslint rule and instead rely solely on strong “compile-time” type checking to prevent you from doing bad things. This does forego the runtime type checking mentioned above, but if your entire application is written in typescript, then you can’t compile/deploy your app with type errors in it anyway, so the loss of runtime type checking has minimal impact.

This applies to both arrow functional components and regular functional components.

Example

If you write a component like so:

const InputThing = (props) => {
    return (
        <input value={props.value} />
    )
}

You’ll either get an info message from the typescript compiler saying:

Parameter ‘props’ implicitly has an ‘any’ type, but a better type may be inferred from usage. ts(7044)

Or an error message saying:

Parameter ‘props’ implicitly has an ‘any’ type. ts(7006)

If you get the first one, you should add "noImplicitAny": true to your tsconfig.json file, otherwise you’re not fully taking advantage of what typescript has to offer.

Given that as the baseline, let’s say you wanted to use the destructured props children and value. You’d write:

const InputThing = ({value, children}) => {
    return (
        <input value={value} />
    )
}

Two errors this time. One for value and one for children. Both saying that they have implicit “any” types, which aren’t allowed. So now it’s time to add type checking into the mix via a typescript interface:

interface InputThingProps {
    value: string
}

const InputThing: FC<InputThingProps> = ({value, children}) => {
    return (
        <input value={value} />
    )
}

No more errors and now we’ve enforced specific property types everywhere that InputThing is used. I’ll note that these typescript types/interfaces can be arbitrarily complex, above and beyond that which you could get from PropTypes alone. This works because the generic-typed FC<> takes a props interface as its type. FC (FunctionComponent) is defined in the react source as follows:

interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

type PropsWithChildren<P> = P & { children?: ReactNode };

So children is allowable by default and becomes unioned with whatever props you provide.

What if you try to use a non-existent prop?

If I try to use a foo prop on InputThing like so:

const Parent = () => {
    return (
        <InputThing foo='bar' />
    )
}

You get an error saying:

Property ‘foo’ does not exist on type ‘IntrinsicAttributes & InputThingProps & { children?: ReactNode; }’ .ts(2322)

To me, this covers all of the PropTypes use cases except for the runtime checking, but in my opinion, this provides little value in a pure typescript react application.

I just came across this, and I’m not sure I understand your argument @ljharb. This currently passes eslint validation:

import React, { PureComponent, ReactNode } from 'react';
import styles from './empty-state.scss';
import classNames from 'classnames';

type Props = {
    isLoading: boolean,
    children: ReactNode,
};

export default class EmptyState extends PureComponent<Props> {
    render() {
        return (
            <div className={classNames(styles.container, this.props.isLoading && styles.loadingState)}>
                {this.props.children}
            </div>
        );
    }
}

With what you said, I would have imagined classes failing as well. Are StatelessComponents (SFC) something special/different that they would be treated differently?

I’m wanting to enforce types being set, but not enforce the PropTypes implementation. (Mostly because I feel the extra code to write out twice just isn’t worth it). Honestly, I think it would be neat if babel could transform the types to PropTypes. Then you get the best of both worlds.

@ljharb I’m not aware of integer in the PropTypes library. I think you’d have to write a custom prop validator to accomplish that, which you could do in ts/js inside the component itself.

I’m not opposed to defining both a props interface and PropTypes, so I agree with your advice to cover all bases. I just wanted to spell out what I believe to be the objective net gain/loss between the two.

This is just another in a long list of reasons to not use arrow functions for functional components.

@ljharb Is there a resource somewhere that goes into this a bit more?

I just came across this, and i use this.

// add this to eslintrc.js
"react/prop-types": [2, { ignore: ['children'] }]

They don’t have an explicit name - only some forms of them will have an implicit name, and it’s not always intuitive which ones will get a name inferred. If there’s no name, debugging is massively inhibited.

This is just another in a long list of reasons to not use arrow functions for functional components.

Thanks for explaining this stuff. Highly appreciated. I managed to fix the error by defining the type of the props variable that goes into the arrow function. Like this:

export interface TaskCardProps {
  title: string;
  details: string;
  category: string;
}
const TaskCard: React.SFC<TaskCardProps> = (props: TaskCardProps) => {
  const { title, details, category } = props;

  return (
    ...
);
};

This seems to work alright. Based on what you said I’m sacrificing type validation, that comes from propTypes at run time.

Can you also tell more reasons for not using arrow functions?