jahed.dev

You don't need PropTypes

Back when React started, it introduced "PropTypes". Initially bundled with React, PropTypes was used to validate the structure of props passed into React components. In a statically typed language, this validation is done by the compiler at compile-time, but JavaScript isn't that so this was a necessary tool to help debug obtuse runtime errors.

import React from "react";
import PropTypes from "prop-types";

const MyComponent = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

MyComponent.propTypes = {
  name: PropTypes.string.isRequired,
};

While this validation is optional and error prone, linters like ESLint had plugins to ensure PropTypes stayed aligned with the actual props used by your component.

A Better Replacement?

PropTypes is no longer bundled with React. It's a separate package. However, React will still validate props using Component.propTypes if it's assigned. Chances are React will drop it entirely in a future release. Type validation seems outside of React's scope and better handled by other tools.

Tools such as TypeScript; a statically typed language which is pretty much JavaScript with type annotations. With TypeScript, you can easily define your props like any other statement.

import React, { FunctionComponent } from "react";

type Props = {
  name: string;
};

const MyComponent: FunctionComponent<Props> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

Compile-time vs Runtime Validation

So, do we still need PropTypes? PropTypes provides runtime validation where as TypeScript provides compile-time validation. These are at different points in the lifecycle so PropTypes does provide benefits. Here's an example:

import React, { FunctionComponent } from "react";
import PropTypes from "prop-types";

type Props = {
  name: string;
};

const MyComponent: FunctionComponent<Props> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

MyComponent.propTypes = {
  name: PropTypes.string.isRequired,
};

const getProps = (): Props => JSON.parse('{ "name": 0 }');

const MyParent: FunctionComponent = () => {
  const { name } = getProps();
  return <MyComponent name={name} />;
};

Here, name is the number 0. Not a string. So why doesn't TypeScript show a problem? Because JSON.parse returns an any type. Its value is only knowable at runtime since a string can parse into any value. It's up to us to validate it, and PropTypes does exactly that. However, there's still an issue. This is what PropTypes logs when it sees this problem:

Warning: Failed prop type: Invalid prop `name` of type `number` supplied to `MyComponent`, expected `string`.
  in MyComponent (at example.tsx:20)
  ...

Assuming example.tsx is a file containing the code above, rather than where we created a MyComponent element, the actual problem is where we call JSON.parse which is assumed to return valid Props. So while PropTypes has alerted us of a problem, we are still required to know where that problem originated. This becomes a bigger problem in a larger application where the error and origin are many stacks apart.

Validate Your Boundaries

Ideally we should not be validating types at the component-level, but instead at our boundaries; where our static code interacts with external values. This way we will know exactly where the type error originated without needing to debug further.

import React, { FunctionComponent } from "react";
import PropTypes from "prop-types";

type Props = {
  name: string;
};

const MyComponent: FunctionComponent<Props> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

const propsSpec = {
  name: PropTypes.string.isRequired,
};

const getProps = (): Props => {
  const result = JSON.parse('{ "name": 0 }');
  PropTypes.checkPropTypes(propsSpec, result, "prop", "getProps");
  return result;
};

const MyParent: FunctionComponent = () => {
  const { name } = getProps();
  return <MyComponent name={name} />;
};

export default MyParent;

Here we've moved validation to our boundary. It's now entirely getProps's responsibility to handle bad input and our errors will originate from it.

Warning: Failed prop type: Invalid prop `name` of type `number` supplied to `getProps`, expected `string`.
  ...

You might have noticed there's not much of a stack trace to help us find getProps. That's because PropTypes is made to be used inside React. It's not made to validate other types of boundaries. For that, we have better options like JSON Schema. We'll also have an opportunity to tell the user when we receive bad input rather than carry on with it like PropTypes does. Let's try it.

import React, { FunctionComponent } from "react";
import Ajv from "ajv";

const propsSchema = {
  type: "object",
  properties: {
    required: {
      name: true,
    },
    name: {
      type: "string",
    },
  },
};

const ajv = new Ajv();

type Props = {
  name: string;
};

const MyComponent: FunctionComponent<Props> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

const getProps = (): Props => {
  const result = JSON.parse('{ "name": 0 }');
  if (ajv.validate(propsSchema, result)) {
    return result;
  }
  throw new Error(ajv.errorsText());
};

const MyParent: FunctionComponent = () => {
  try {
    const props = getProps();
    return <MyComponent name={props.name} />;
  } catch (error) {
    console.error(error);
    return <div>{error.toString()}</div>;
  }
};

export default MyParent;

Note: In a real scenario, you'll likely deal with Promises and async state rather than a simple try-catch. Same idea, different statements.

This will show the user an error message and also log it with a stacktrace:

Error: "data.name should be string"
  getProps example.tsx:31
  MyParent example.tsx:36
  ...

Perfect! Now the only issue is the duplication between our JSON schema and TypeScript type. It risks going out of sync. There are tools to generate JSON schema from TypeScript types so we can use that in our build pipeline and import the JSON schema instead to reduce our workload.

Conclusion

In conclusion, you really don't need PropTypes. If you're worried about type safety, use TypeScript. PropTypes only solves a very specific problem at runtime and will only give you a hint that something is wrong. It's a developer tool for runtime debugging. TypeScript on the other hand covers the entire codebase, encouraging better practices throughout. And there are standard tools, like JSON Schema, which are focused on runtime validation for both development and production.

Thanks for reading.