TypeOfNaN

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.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli