You Deserve More than PropTypes
I’ll start with reasons why I think PropTypes are not good enough, and later I’ll show what TypeScript gives you to solve these problems and improve your React code even more on top of it.
I’d like to be clear — I’m not bashing prop-types — It’s a really good library, but the last publish was 9 months ago. As I’m writing this, it’s November 2019, and there are much better alternatives for prop types.
I’ve chosen TypeScript because of its popularity, but my arguments fit any language with first-class type composition you can use to build React apps (Flow, Reason, Kotlin, Scala).
Why?
It’s easy to half-ass PropTypes
I’ve seen too many of lines like this:
js
//eslint-disable-next-line react/forbid-prop-types
js
//eslint-disable-next-line react/forbid-prop-types
Few codebases leverage PropTypes to their full potential — mostly libraries (see Reach UI tabs).
I find exporting propTypes uncommon. Instead of using exported common types, developers either use PropTypes.object or copy PropTypes.shape from another component.
Maybe it is hard to remember that you strip them out in production build, and that’s why the devs I’ve met don’t want to make them too big and heavy?
PropTypes.func is not enough
Functions make stuff happen. They are pretty important. Types of functions are important too. Stating that a prop is just a function, doesn’t document intent. You still need to read the implementation to get the slightest idea of what’s happening.
Take a look at the props above. onSelectVideo
takes a video and returns a
unit. This is a lot more information than “onSelectVideo
is a function”. We
could argue that the name of the function should be enough, but what if a
possibility to select multiple videos was added later, as an additional feature?
If someone forgot to change the function name, PropTypes.func
would still fit,
and some other poor soul would get surprised by a runtime error.
Optional by default
Optional is a bad default for application code.
I do agree that nullable by default is a good design choice in some cases. GraphQL is a perfect example. Responses stitched from many data stores may return partial data. This is the complexity we have to handle.
And I’d say we have about enough of it. We should avoid introducing more complexity ourselves. Every optional field without a default of the same type increases cyclomatic complexity.
Does this person have a car? Maybe. I live in a big city; I don’t have one too.
But is an empty object {}
really a valid car for our app? Do we display an
error message here? Did we just forget to write isRequired
, or are we okay
with cars without license plates?
Typing isRequired
is yet another small decision for a programmer. The fact
that stating that a prop is nullable is an easier way allows to accidentally
introduce complexity. isOptional
instead of isRequired
would be a better API
design.
type Props = ?
TypeScript is much better in describing React component props than PropTypes. Let’s look at how it solves the problems I’ve mentioned before.
Harder to half-ass
Add strict: true
to your tsconfig.json, stray from any
and now you’re forced
to maintain a decent level of type safety. Also, it’s pretty obvious, even
before a morning coffee, that it has no runtime cost.
Typing functions
(selected: Video) => void
. Pretty easy, amiright? Programming, even OOP, is
mostly about using functions to do stuff. Ability to describe the type of a
function is quite useful.
Required by default
In TypeScript, you gotta stick this ?
every time you want an optional
property.
And look at what else we get!
I could talk about subtyping and Liskov Substitution Principle, but I’ll
simplify it a little bit. If it’s a button, it should be buttony.
Props you expect on a button should be accepted by all of your design system buttons.
What do I expect? At least onClick
, onFocus
, disabled
, className
, and style
.
We can handle all attributes of HTML <button>
element, including all global attributes
with a simple spread
Do you want your Button to be inferior to a button? I don’t think so.
But what if my component comes “batteries included” and I don’t want to accept
all props of the component I’m building upon?
Only like… most of them? We can Omit what we don’t like. Just like that.
tsx
interface JoinMeetingButtonextends Omit<ButtonProps, "onClick">,Pick<Meeting, "id"> {}
tsx
interface JoinMeetingButtonextends Omit<ButtonProps, "onClick">,Pick<Meeting, "id"> {}
Union Types
The anchors and the buttons often look the same in the mockups, but they are different kinds of animals. We want to reuse the styling and behavior between them and make choosing the right one for the job effortless.
We can use union types to build a Button component which renders an anchor,
given a href
prop and renders a <button>
otherwise.
tsx
import { ComponentProps } from "react";interface ButtonAsAnchorProps extends ComponentProps<"a"> {href: string;}interface ButtonAsButtonProps extends ComponentProps<"button"> {href?: undefined;}type ButtonProps = ButtonAsAnchorProps | ButtonAsButtonProps;function Button({ className: propsClassName, ...rest }: ButtonProps) {const className = ["Button", propsClassName].join(" ");if (rest.href !== undefined) {return <a className={className} {...rest} />;}return <button className={className} {...rest} />;}
tsx
import { ComponentProps } from "react";interface ButtonAsAnchorProps extends ComponentProps<"a"> {href: string;}interface ButtonAsButtonProps extends ComponentProps<"button"> {href?: undefined;}type ButtonProps = ButtonAsAnchorProps | ButtonAsButtonProps;function Button({ className: propsClassName, ...rest }: ButtonProps) {const className = ["Button", propsClassName].join(" ");if (rest.href !== undefined) {return <a className={className} {...rest} />;}return <button className={className} {...rest} />;}
Picking
a subset
We can select properties from our types with Pick
.
tsx
type MeetingInfoProps = Pick<Meeting, "date" | "organizer">;const MeetingInfo = ({ date, organizer }: MeetingInfoProps) => (<>{new Date(date).toLocaleString()} • {organizer.name}</>);
tsx
type MeetingInfoProps = Pick<Meeting, "date" | "organizer">;const MeetingInfo = ({ date, organizer }: MeetingInfoProps) => (<>{new Date(date).toLocaleString()} • {organizer.name}</>);
Imagine that Meeting is a type of data we get from the backend. We want to
show MeetingInfo — a date and organizer of the meeting and we don’t really care
about the type of these date
and organizer
props. We care about their
origin. They come from the Meeting type and that’s what’s important for this
component. Will this component break when the representation of our meetings
change? Yes. And we want it to.
Summary
PropTypes are not first class. They’re a library trying to implement what is
often a language feature. If you’re building an app, you don’t need runtime
typechecking. Try swapping prop-types
for TypeScript or Flow and
tweet me what you think.
You can see the types I’ve written about used together in the sandbox below.