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.