TypeScript vs. PropTypes

How exactly are TypeScript and React PropTypes related? Generally they both perform type checking, but they differ in when they're run and how they report errors.

Library When it executes Where errors are reported
TypeScript While editing or building In-editor code highlighting; build output
PropTypes While executing locally* Warnings in JS console

What does that mean for codebases that are transitioning to TypeScript? What's worked well for me has been to treat PropTypes as legacy: new components get TypeScript definitions for their props, and components that are being re-written to TypeScript have their PropType declarations converted to a TypeScript interface.

Example - Porting PropTypes

Assume you're looking to port the PropTypes of a non-trivial component, something like gatsby-image's <Image> component:

React

const fixedObject = PropTypes.shape({
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired,
  src: PropTypes.string.isRequired,
  srcSet: PropTypes.string.isRequired,
  base64: PropTypes.string,
  tracedSVG: PropTypes.string,
  srcWebp: PropTypes.string,
  srcSetWebp: PropTypes.string,
  media: PropTypes.string,
});

Image.propTypes = {
  resolutions: fixedObject,
  fixed: PropTypes.oneOfType([fixedObject, PropTypes.arrayOf(fixedObject)]),
  fadeIn: PropTypes.bool,
  durationFadeIn: PropTypes.number,
  title: PropTypes.string,
  alt: PropTypes.string,
};

To convert this to TypeScript, I'll start with this conversion table:

PropType TypeScript
PropTypes.number number
PropTypes.string string
PropType.bool boolean (not bool or Boolean)

So let's take a first stab at writing an interface to represent Image.propTypes:

TypeScript

interface ImageProps {
  fadeIn: boolean;
  durationFadeIn: number;
  title: string;
  alt: string; // each line of an interface ends in a semicolon
}

Now what about:

  • isRequired vs. optional props?
  • Objects with nested properties?
  • oneOfType?

Converting ".isRequired"

TypeScript inverts how you declare that a property is required; instead, you mark optional props with a "?" in the field name.

Updating the example,

interface ImageProps {
  fadeIn?: boolean;
  durationFadeIn?: number;
  title?: string;
  alt?: string;
}

Converting props with nested properties

In the example fixedObject is a helper prop-type that captures the shape of props.resolutions. In TypeScript it can be declared as a separate type.

interface FixedObject {
  width: number;
  height: number;
  src: string;
  srcSet: string;
  base64?: string;
  tracedSVG?: string;
  srcWebp?: string;
  srcSetWebp?: string;
  media?: string;
}

interface ImageProps {
  resolutions: FixedObject;
  fadeIn?: boolean;
  durationFadeIn?: number;
  title?: string;
  alt?: string;
}

Converting "oneOfType"

For oneOfType you can use a TypeScript union type.

interface ImageProps {
  resolutions: FixedObject;
  fixed?: FixedObject | Array<FixedObject>;
  fadeIn?: boolean;
  durationFadeIn?: number;
  title?: string;
  alt?: string;
}

The fixed prop can now receive either an object or array of objects, like in the original PropTypes declaration.

Wrapping Up - Usage and Next Steps

Now that ImageProps is fully converted, the React component needs to reference it to get the benefits of Typescript's type checking.

Assuming the <Image> component is a functional (and not class-based) React component, let's update the component declaration:

const Image: React.FunctionComponent<ImageProps> = (props) => {
  // ...
};

And that's it! Now you should see type suggestions and warnings in your editor as you make changes to the file. This conversion process gets easier with some practice.

If you're using VSCode, be sure to check their TypeScript guide for the latest features - it provides a lot of capabilities you might not expect.

© 2021 David Mann. All content provided on this site, code or otherwise, is licensed and/or copyright as specified below: