React - March 25, 2024
Component Props using Discriminated Unions
5 minute read
Discriminated Unions are a widely used pattern in TypeScript but are often overlooked when it comes to defining the types for React Component props. In this article, we'll look at how we can use Discriminated Unions to define the props for a React Component.
Example Usecase
Let's consider we're asked to create a UserProfileCard
component that can be used to display information about user profiles. The component should be able to display two different types of user profiles - a regular user profile and an admin profile. The component should always display the user's name, but depending on the type of profile (user or admin), it should display additional information. In case of a regular user profile, it should display the user's email address, and in case of an admin profile, it should display the permissions of the admin user. Something like this:
John D.
user
example@email.com
John D.
admin
Implementation without Union Discriminators
A good first approach to implement the UserProfileCard
component would be to start with defining the props we need to properly display all the information. Looking ath the examples above, we can deconstruct them into the following set op props:
name
- The name of the usertype
- The type of the user profile (eitheruser
oradmin
)email
- The email address of the userpermissions
- The permissions of the admin UserProfileCard
A junior version of myself would probably implemented the component like this (simplified JSX for readability):
export type UserProfileCardProps = {
name: string;
type: 'user' | 'admin';
email?: string;
permissions?: string[];
}
export function UserProfileCard(props: UserProfileCardProps) {
return (
<div>
<p>{props.name}</p>
{props.email && <p>{props.email}</p>}
{props.permissions && props.permissions.map((permission) => <span key={permission}>{permission}</span>)}
</div>
);
}
While this implementation defenitely works, there are some drawbacks. The main issue is that the UserProfileCard
component is not fully type-safe. With the current implementation we can do all kind of crazy things that are not in line with the requirements. We could:
- Display an
admin
profile without providing thepermissions
. - Display a
user
profile and provide thepermissions
prop. - Display a profile with both
email
andpermissions
props or without any of them.
<UserProfileCard
name="Jane Cooper"
type="admin"
email="example@email.com" // 👈 An `admin` card shouldn't receive an email
/>
<UserProfileCard
name="Jane Cooper"
type="user"
permissions={['read', 'write']} // 👈 A `user` card shouldn't receive permissions
/>
<UserProfileCard
name="Jane Cooper"
type="user"
permissions={['read', 'write']} // 👈 A `permissions` prop ...
email="example@email.com" // 👈 AND a `email` prop
/>
Union Descriminators to the Rescue
Let's look at how we can leverage TypeScript and Union Discriminators to improve our UserProfileCard
component. The magic fully lies with the way in how we define the type of our props. Lets start with the prop that both the user
and admin
profile share:
type UserProfileCardProps = {
name: string;
}
Next, we define the props for the two different types of profiles. To distinguish between the two types of profiles, we add a type
property. This is the property that will be used as the discriminator. We also add the additional properties that are specific to each type of profile:
type User = {
type: 'user';
email: string;
};
type Admin = {
type: 'admin';
permissions: string[];
};
Finally, we add the User
and Admin
types to the UserProfileCardProps
type to create the union:
export type UserProfileCardProps = {
name: string;
} & (User | Admin);
If you want to see the full implementation, you can expand the collapsible below:
See the full implementation
Seeing it in Action
Now that we have implemented the UserProfileCard
component using Union Discriminators, let's see how it behaves when we try to pass the wrong props to the component. To demonstrate this we will use the same example as we used earlier.
<UserProfileCard
name="Jane Cooper"
type="admin"
email="example@email.com" // 👈 Property email does not exist on type
/>
<UserProfileCard
name="Jane Cooper"
type="user"
permissions={['read', 'write']} // 👈 Property permissions does not exist on type
/>
As we can see, TypeScript now correctly points out that we are trying to pass the wrong combination of props to the UserProfileCard
component. This makes sure that we are not accidentally passing the wrong combination of props to our component. This is a great way to make sure that our implementation is fully in line with the requirements.