DEV Community

Cover image for Understanding Conditional TypeScript Props: Building Adaptable Components
Sawan Bhattacharya
Sawan Bhattacharya

Posted on

Understanding Conditional TypeScript Props: Building Adaptable Components

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

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>
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

So for this component, if we have to define a type, then this is how most of us will write it

type Prop = {
    id: number;
    color: string;
    typeShape: "circle" | "square" | "rectangle";
    radius?: number;
    sideLength?: number;
    width?: number;
    height?: number;
};
Enter fullscreen mode Exit fullscreen mode

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

To fix this, we are going to take leverage of union type 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.

type Props = {
    id: number;
    color: string;
} & (
    | { typeShape: "circle"; radius: number }
    | { typeShape: "square"; sideLength: number }
    | { typeShape: "rectangle"; width: number; height: number }
);
Enter fullscreen mode Exit fullscreen mode

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

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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

export function Shape(props: ICircle | ISquare | IRectangle) {
// body will be same
}
Enter fullscreen mode Exit fullscreen mode

In conclusion, we have achieved conditional props with interfaces, just like we did with types.

Top comments (1)

Collapse
 
thecodersden profile image
Rajdip Bhattacharya

Bookmarked!