Understanding Conditional TypeScript Props: Building Adaptable Components
Tags: typescript, webdev, tutorial, react
Published: 2023-07-23 15:41:49

Have you ever come across situations where certain properties of a component should only appear under specific conditions or when particular data is provided? This can be a real challenge, but fear not! TypeScript offers a nifty feature called 'conditional types' that can help us deal with such scenarios effectively. In this blog, we'll explore how conditional TypeScript props allow us to control the availability of properties based on different conditions, making our components more adaptable and secure. Let's dive in and discover the magic of TypeScript's conditional types!

To achieve this, TypeScript provides two primary ways: types and interfaces. In this blog, we'll start by exploring conditional types using `types`, and later, we'll dive into `interfaces` to further enrich our understanding.

## Conditional props using `types`

Let's say you are making a component called `Shape` where we can define the id, colour, type of shape, and their dimensions, and according to that it's going to render out the shape.

Here is the component
```tsx
export function Shape(props: Prop) {
    const { id, typeShape, color } = props;

    if (typeShape === "circle") {
        const { radius } = props;
        return (
            <div
                key={id}
                style={{
                    width: `${radius}rem`,
                    height: `${radius}rem`,
                    borderRadius: `1000px`,
                    backgroundColor: `${color}`,
                    alignItems: `center`,
                    justifyContent: `center`,
                    display: `flex`,
                }}
            >
                Circle
            </div>
        );
    } else if (typeShape === "square") {
        const { sideLength } = props;
        return (
            <div
                key={id}
                style={{
                    width: `${sideLength}rem`,
                    height: `${sideLength}rem`,
                    backgroundColor: `${color}`,
                    alignItems: `center`,
                    justifyContent: `center`,
                    display: `flex`,
                }}
            >
                Square
            </div>
        );
    } else if (typeShape === "rectangle") {
        const { width, height } = props;
        return (
            <div
                key={id}
                style={{
                    width: `${width}rem`,
                    height: `${height}rem`,
                    backgroundColor: `${color}`,
                    alignItems: `center`,
                    justifyContent: `center`,
                    display: `flex`,
                }}
            >
                Rectangle
            </div>
        );
    }
}
```

So for this component, if we have to define a type, then this is how most of us will write it
 ```ts
type Prop = {
    id: number;
    color: string;
    typeShape: "circle" | "square" | "rectangle";
    radius?: number;
    sideLength?: number;
    width?: number;
    height?: number;
};
```

The current problem with this type definition is that if the `typeShape` is set to "circle", it allows the usage of properties like `width` or `sideLength` in addition to the expected `radius`, because we have defined this property optional. This flexibility can lead to potential errors, and it's not as clear which properties are valid for a particular shape.

![code with no type error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifom1ye03pecjdw3qt5e.png)

To fix this, we are going to take leverage of [union type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) in TypeScript. Union types allow us to combine multiple types into a single type, giving us the flexibility to represent a value that can be one of several types.

In our case, we are going to use union types to construct a more refined type definition for the Prop type. The common part of the type have properties `id`, and `color`. We then utilize the `&` (intersection) operator to combine this base type with different conditional types, each corresponding to a specific shape.

```ts
type Props = {
    id: number;
    color: string;
} & (
    | { typeShape: "circle"; radius: number }
    | { typeShape: "square"; sideLength: number }
    | { typeShape: "rectangle"; width: number; height: number }
);
```
Now you can see that the editor is throwing an error, which says `Type '{ id: number; typeShape: "circle"; radius: number; color: string; width: number; sideLength: number; }' is not assignable to type 'IntrinsicAttributes & Props'.
  Property 'width' does not exist on type 'IntrinsicAttributes & { id: number; color: string; } & { typeShape: "circle"; radius: number; }'.`

![Code throwing error after changing the type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n7iynwjqz9e7srwx8l1x.png)

This is precisely what we aimed to achieve with the implementation of conditional types. By using conditional types, we have transformed our type system, making it more precise and reliable. TypeScript's static analysis capabilities enhance our development experience and promote the creation of robust and type-safe components.

The error message demonstrates that TypeScript recognizes the specific shape-related properties that should be allowed for each shape type. In this case, it correctly enforces that `width` and `sideLength` properties are not allowed for the "circle" shape, and the correct property for a "circle" shape is `radius`.

## Conditional props using `interface`

Now let's explore how we can achieve the same result using interfaces. Interfaces are another essential feature of TypeScript that allows us to define custom types and object shapes. Similar to types, interfaces provide a way to create reusable type definitions for our components and data structures.

```ts
interface IShape {
    id: number;
    color: string;
}

interface ICircle extends IShape {
    typeShape: "circle";
    radius: number;
}

interface ISquare extends IShape {
    typeShape: "square";
    sideLength: number;
}

interface IRectangle extends IShape {
    typeShape: "rectangle";
    width: number;
    height: number;
}
```
Here, the extends keyword acts as an intersection (&), effectively creating a relationship where the properties of IShape will be shared across all other shape interfaces.

By using extends, we establish a connection between the base IShape interface and the shape-specific interfaces (ICircle, ISquare, and IRectangle). This relationship ensures that all shape interfaces inherit the properties defined in IShape (id and color).

Now to make it optional, we have to use union in the component prop

```tsx
export function Shape(props: ICircle | ISquare | IRectangle) {
// body will be same
}
```
In conclusion, we have achieved conditional props with `interfaces`, just like we did with `types`.