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

read
write

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 user
  • type - The type of the user profile (either user or admin)
  • email - The email address of the user
  • permissions - 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 the permissions.
  • Display a user profile and provide the permissions prop.
  • Display a profile with both email and permissions 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.