Use TypeScript Interfaces Instead of ESLint PropTypes Validation
Tags: react, typescript, eslint
Published: 2021-10-23 17:04:40

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](https://github.com/yannickcr/ESLint-plugin-React/issues/2275)

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:

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

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

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

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

```tsx
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!