Conditional component props in React with Typescript
Nick Scialli
March 16, 2023
When writing React, having strictly-typed components is helpful in preventing you from making mistakes. However, if you need a particularly complex component type signature with conditional types, it can be challenging to get right.
I this article, we’ll explore a couple ways we can get correct type-checking out of our prop type.
A contrived example
Let’s make a component, User
, that takes two props: an authenticated
boolean prop. If authenticated
is true
then we’ll also want the user’s permission level (basic
or admin
). If the user is not authenticated, their permission level should be guest
.
Therefore, the following are valid component calls:
// Valid
<User authenticated={false} level="guest" />
// Also valid!
<User authenticated={true} level="basic" />
// Also valid!
<User authenticated={true} level="admin" />
Conversely, the following component calls are invalid and should result in Typescript compilation errors:
// Not valid, `level` must be either "basic" or "admin"
<User authenticated={true} level="guest" />
// Also not valid, `level` must be "guest"
<User authenticated={false} level="basic" />
A naive solution that doesn’t work
Our first idea might be a relatively straightforward typing:
type Props = {
authenticated: boolean;
level: 'guest' | 'basic' | 'admin';
};
const User = (props: Props) => {
if (props.authenticated) {
return <>Profile ({props.level})</>;
}
return <>{props.level}</>;
};
This doesn’t work! With this basic typing, level
can be any of the options regardless of what authenticated
is.
We need a better solution—one that lets us enforce the type of level
conditionally.
First working solution: union type
One quick option in this case is a union type for the props object. In one of the union types, authenticated
will be true
and will require the level
to be either "basic"
or "admin"
. In the other union type, authenticated
will be false
and level
will only be "guest"
.
type Props =
| {
authenticated: true;
level: 'basic' | 'admin';
}
| {
authenticated: false;
level: 'guest';
};
const User = (props: Props) => {
if (props.authenticated) {
return <>Profile ({props.level})</>;
}
return <>{props.level}</>;
};
And this works! When you call the component with authenticated
set to true
, typescript recognizes this must be the first union type and the level
can be either "basic"
or "admin"
. If authenticated
is false
, level
can only be "guest"
.
Second working solution: generics
This second solution is a bit more verbose but in some cases may be preferrable. We can use a generic type that represents the authenticated
type and use the ternary operator syntax to create a conditional type:
type Props<T extends boolean> = {
authenticated: T;
level: T extends true ? 'basic' | 'admin' : 'guest';
};
const User = <T extends boolean>(props: Props<T>) => {
if (props.authenticated) {
return <>Profile ({props.level})</>;
}
return <>{props.level}</>;
};
This also works! We condition the level
type based on whether T extends true
(i.e., T
is true
). If that’s the case, the level
type will be "basic" | "admin"
. If false
, the level
type will be "guest"
.
Which option to choose
Chosing one of these options is situation-dependent. I usually favor the generics approach because you don’t have to retype the props multiple times—it’s just one object. However, the generics approach doesn’t easily handle the situation where you might want to conditionally require or not require a prop altogether. For example, if our requirement changed that we don’t want the level
prop at all when the user is not authenticated, this is actually quite hard to do with the generics approach, but trivial with the union approach:
type Props =
| {
authenticated: true;
level: 'basic' | 'admin';
}
| {
authenticated: false;
};
Conclusion
There are multiple ways you can conditionally type props for a React component: union objects and generics. While I tend to favor generics, either approach works just fine and you’ll likely find situations where one approach is far superior to the other.
Nick Scialli is a senior UI engineer at Microsoft.