Logo provash.dev

14.11.2022

Using typescript conditional types with react to better handle conflicting props

Showcasing benifits of conditional typing

This article assumes you have a basic understanding of Typescript and ReactJs.

Imagine a scenario where you have a button component inside of a navigation bar. Kinda like the burger nav button on websites. Now, you have a property placeLeft and placeRight in the button, which will apply certain stylings that will place the button either on the left side, or on the right side of the nav bar. What would happen if you pass both placeLeft and placeRight to the component?

Second scenario, you have a UserInfoCard component, where you can pass withImage prop (optional). This withImage prop also requires another prop imageLink to work properly. Otherwise there is weird behavior and an empty image box is just getting displayed.

Now, all these scenarios can be mitigated with sanitary checks in basic javascript. But with the help of Typescript and its conditional prop, you can easily solve this issue during the development time and remove unwanted errors during run-time. With Typescript interfaces, it’s already a big help over traditional react prop-type checking. Beside catching errors during build-time / dev-time, you will get a much better developer experience if you’re using a text editor that utilizes typescript benefits like VSCode.

Let’s build our first scenario. We have the following button component with basic type guards:

interface IProps {
  children: React.ReactNode;
  placeLeft: boolean;
  placeRight: boolean;
}

export const Button = ({ children, placeLeft, placeRight }: IProps) => {
  return (
    <button
      style={{
        ...(placeLeft && { alignSelf: "flex-start" }),
        ...(placeRight && { alignSelf: "flex-end" }),
      }}
    >
      {children}
    </button>
  );
};

We’re assuming the component will be used inside of a flex-box wrapper. Our component is smart enough to only apply certain styling when the valid props are present. But if both props are passed, it will apply both styling yielding an unwanted result. Here’s how conditional prop can help us out, we need to remove the placeLeft and placeRight from interface IProp and extract it to a conditional type, like this:

type TConditionalProps = { placeLeft: boolean; placeRight?: never } | { placeLeft?: never; placeRight: boolean };

Then we can use it like this:

import React from "react";

interface IProps {
  children: React.ReactNode;
}

type TConditionalProps = { placeLeft: boolean; placeRight?: never } | { placeLeft?: never; placeRight: boolean };

export const Button = ({ children, placeLeft, placeRight }: IProps & TConditionalProps) => {
  return (
    <button
      style={{
        ...(placeLeft && { alignSelf: "flex-start" }),
        ...(placeRight && { alignSelf: "flex-end" }),
      }}
    >
      {children}
    </button>
  );
};

Now, if you try to pass both placeLeft and placeRight, typescript will complain that both cannot be passed. If you pass neither of the properties, typescript will also complain. Khachangggg…🛠️

To resolve our second scenario, we can do something like following:

import React from "react";

interface IProps {
  name: string;
}

type TConditionalProps = { withImage?: never; imageLink?: never } | { withImage: true; imageLink: string };

const UserInfoCard = ({ name, imageLink, withImage }: IProps & TConditionalProps) => {
  return (
    <div>
      <span>{name}</span>
      {withImage && <img src={imageLink} alt="our user dude / gal" />}
    </div>
  );
};

In this case, you can pass only the name to the component. But, if you want to pass either withImage or imageLink prop, it will complain that other one is required as well. So either both need to be present, or none. You an of course argue that this is not required as we can easily just check imageLink prop and render the image, but this example just assume you might have a requirement like this for some other use-cases.

Now, these works, but why it works is also a good question. As you can see we are using typescript types instead of interfaces, types can be conditionally joined with | symbol. In our first scenario, we used the following type:

type TConditionalProps = { placeLeft: boolean; placeRight?: never } | { placeLeft?: never; placeRight: boolean };

Here, we’re telling typescript that, if the prop placeLeft is present, you should not allow placeRight, by saying placeRight is now optional and its of type never. Never, is a special type in typescript which you can define to say you’re not expecting this type in this case. The same is true for our second scenario:

type TConditionalProps = { withImage?: never; imageLink?: never } | { withImage: true; imageLink: string };

We’re telling typescript that either both props should be present, or none should be present. Important to note that, when using never, you need to make the prop itself optional. Otherwise there will be errors as typescript will assume it also requires these props.

Hope this helps you to better type-gurad your react components. Thanks for reading. ✌🏼