Mutually Exclusive React PropTypes with TypeScript
Sometimes when making a React component, you define prop types that allow users to pass one of N types of props. For instance, we might have some form of input component that could either be a standard <input/>
or a larger <textarea/>
. These two HTML elements have certain properties that the other does not.
How I wish it worked
TypeScript can pleasantly surprise you with how smart it is, then make you question its stupidity minutes later (at least as of 2.6.2
.) My first attempt at exclusive prop types was succinct, and seemed like it would make sense for TypeScript to follow along with; I’d define a set of base props, and extend that with two sets of exclusive props. From there I could just check if the properties on those types were truthy, and then it would know which type it was.
Unfortunately this doesn’t work. TypeScript won’t allow you to check this.props.type
because TextareaProps
doesn’t have that property. We could get around this by asserting the type in the conditional, but that wouldn’t handle the else case, so we’d have to assert there too. In any case, assertions are usually a sign that something’s not quite right in your approach.
How to make it work
Method 1 - Shared Prop
If you don’t mind having someone pass another prop, a boolean prop shared between the two interfaces is a great way to differentiate between them, and have TypeScript follow along. You can also provide a default by making the desired default props interface’s shared prop optional with a ?
:
If you have more than 2 exclusive prop sets, you can make the shared property the value of an enum. Setting a default still works the same as a boolean prop, just make it optional.
Method 2 - Type Guards
If you want to avoid passing in an extra prop, you can do so with a user defined type guard function (More explanation in this handy guide.) TypeScript allows you to type the return of a function to be whether or not something is of a certain type, by making it arg is MyType
. So we can just make a function that checks for one of the properties of your type like so:
We still have to assert props as InputProps
in the type guard function, because we can’t access the type
property on TextareaProps
objects since it’s not defined. We can further clean up our type guard function by adding type: undefined;
as a property on the TextareaProps interface, so that TypeScript knows we intend to check against it:
A warning about type guard functions though, TypeScript doesn’t keep track of the type determination if you assign it to a variable, so the function has to be invoked in the conditional. If you assign it to a variable, it thinks it’s just any old boolean:
Final thoughts
Switching to TypeScript has been a huge boon to our team’s productivity, but sometimes it comes to a grinding halt because of these unintuitive work-arounds. But the language is always improving, and I look forward to seeing checks like these becoming more intuitive.
If you want to work with a great team of folks who have been putting together an excellent TypeScript codebase, check out the MyCrypto project on GitHub. Big thanks to Henry Nguyen for working this out with me.