You Deserve More than PropTypes
Nov 17, 2019 · ☕☕ 7 min readI’m starting with reasons why I think PropTypes are not good enough, and later I’m showing 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 with
//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 just 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 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.
easy 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.
optional by default?
In TypeScript, you gotta stick this ?
every time you want an optional
property.
And look at what else we get!
ComponentProps<"
button"
>
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.
Omit<LinkProps, "
to"
>
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.
interface JoinMeetingButton
extends 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.
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} />;
}
Pick<Meeting, "
date"
| "
organizer"
>
We can select properties from our types with Pick
.
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. Also, we avoid
introducing new names
(e.g.<MeetingInfo author={meeting.organizer} />
).
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.