A problem that I see quite frequently with React components is that as they grow over time, they need to accept one set of props, OR another set of props.
Lets look at a real world example that involves uploading documents to an offer on a real estate listing.
Evolving props over time
DocumentUploader needs to accept enough information to identify the offer where the document should be uploaded to. When the offer is still in a draft state, just the id is enough to identify the offer.
But once the offer is submitted, just the offer id is no longer enough to properly identify the offer; we also need the id of the listing that the offer has been submitted to. Lets add those two new props to the component.
Now we can pass either
draftOfferId, OR we can pass
listingId. We could document that relationship in the documentation for this component. But wouldn’t it be nice if TypeScript could enforce that relationship for us so that we don’t have to rely on the next developer to (gasp) read the documentation?
We can use TypeScript’s union (OR) types to enforce that relationship. We can define a type that accepts either
Now if we try to pass the wrong combination of props, TypeScript will throw an error:
Accessing the props inside the component
One difficulty with defining our props as a union type is that we can no longer access the props directly because TypeScript can’t be certain which props exist and which don’t.
In order to safely access the props, we can use the
in keyword to check if a property exists on the props object.
But sometimes that
if structure is awkward in React components. If you would instead prefer to destructure all your props at the top of the component, you can structure the
in checks differently.
The advantage of the
if structure is that inside of the if you can be certain for example that
listingId is a string, but in the ternary structure
listingId when used later might be undefined and you’ll have to handle that case. That might be fine or it might be annoying, depending on your use case.
Now this is where things get fun!
A discriminated union is just a union type where all variants share a common property (often called
type but can have any name) that can be used to distinguish between the different variants of the union.
So now instead of using the
in keyword to check if a property exists on the object, we can use the
type property (which always exists) to check which variant of the union we were given.
One advantage of a discriminated union is that you get autocomplete when comparing against the
type property. TypeScript knows that the
type property can only be one of the two strings (in this example) and will autocomplete those strings for you.
in method that we used for our non-discriminated/regular union, you don’t get autocomplete for the property name. The
in method is still strongly typed after a fashion though because if you mess it up you will get a type error (because the type doesn’t get properly narrowed), just not directly on that line.
So you’ve got this discriminated union and you want to go through each of the possible cases.
And that’s great, until someone goes and adds another possible type:
Now that switch statement that you wrote needs to be updated to handle the new case, but unless you remembered that it exists or you do some kind of project-wide search for “OfferSpecifier” or something, it’s really easy to forget to change that switch at all and not discover this bug until it’s reported by a user.
But fear not! Another advantage of using a discriminated union is that we can make TypeScript enforce that we handle all the cases in our union, and throw an error if we don’t (or if more cases are added later). We do that by asserting that the type of the union is
never inside of the
default case of the switch statement.
The only way that
offerSpecifier can be of type
never at the end of the switch statement is if every possible valid case has been handled. If a new case is added to the union, then the
default case will throw an error because it is no longer exhaustive.
You can codify this
never functionality with an
assertNever helper function:
Bonus Tip: Passing Union Props Between Components
I’ve found that the union type is often a group of props that I have to “prop drill”, or pass around to several components or hooks. To avoid having to destructure the props with the
in keyword in parent components that don’t even make direct use of them, instead of mixing the union with the rest of the props, it can be helpful to pass around the union in its own object.