Use TypeScript Interfaces Instead of ESLint PropTypes Validation

Sep 13, 2021
typescript-vs-eslint

In my TypeScript experimentation, I encountered an ESLint error I couldn't seem to resolve. The error had to do with proptype validation failing in the presence of what I believed to be a sufficient TypeScript interface to cover the props used by the functional component. So like any curious coder, I went on a huge tangent of discovery to figure out what was really happening and what I could do about it.

Here's the ESLint issue for reference: Props validation with TS StatelessComponent (SFC) type fails

This post is mostly a restating of the info I dropped in that issue in case anyone hits the same problem and wants a bit more of a detailed explanation about what's happening.

In a nutshell, the issue is the result of using FC<MyProps> to define a functional component in the presence of the ESLint prop-types rule. Under these circumstances, ESLint's prop-types plugin requires you to provide prop type definitions for internal props such as "children", even though TypeScript already covers this with the FC and SFC types.

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.

Example

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

If you write a TypeScript 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. Hey, look! PropTypes is in there too!

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

If you 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)

Which means you now have a reliable, TypeScript-only mechanism to define your props and ensure you provide them 100% correctly across all of your components!

Conclusion

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 hope you found this semi-deep-dive helpful to explain the situation a bit more. It's easy to encounter a problem like this and turn away from PropTypes, TypeScript, or even worse: both. The goal of this post isn't to say that TypeScript interfaces are better than PropTypes, or vise versa. It's instead to let you know that whichever path you choose, you're covered!

Related Posts