How can I make a TypeScript interface prop depend on another prop within the same interface?

Viewed 1377
type ButtonVariant = 'action' | 'hero';

type Size = 'small' | 'medium' | 'large';

export interface ButtonProps {
    variant: ButtonVariant;
    size?: Size;
}

export default function Button(props: ButtonProps): ReactElement {
    const { variant } = props;

    if (variant === 'hero')) {
        return <HeroButton {...props} />;
    }

    if (variant === 'pill') {
        return <PillButton {...props} />;
    }
}

In the above code, I have two variants of a React button: HeroButton and PillButton.

Both buttons expect "ButtonProps" as their prop type. However, I would like to make HeroButton only expect a size of "medium" or "large". PillButton should accept "small", "medium" or "large" as its size prop.

Question: how can I write the ButtonProps interface such that the Button component allows (and suggests in VS Code) 'small' | 'medium' | 'large' as the size prop when the variant prop is "pill", but only allows 'medium' | 'large' when the variant prop is "hero"?

Note: I've looked into TypeScript Discriminated Unions to solve this, but haven't managed to get it working.

2 Answers

Try this:

export interface HeroButtonProps {
    variant: 'hero';
    size?: 'medium' | 'large';
}

export interface PillButtonProps {
    variant: 'pill';
    size?: 'small' | 'medium' | 'large';
}

export type ButtonProps = HeroButtonProps | PillButtonProps;

Then change HeroButton to only accept a HeroButtonProps and likewise for PillButton.

Something like so should work

type DiscUnion =
| { variant: "hero"; size?: "medium" | "large" }
| { variant: "pill"; size?: "small" | "medium" | "large" };

export default function Button(props: DiscUnion): any {
    if (props.variant === "hero") {
        const w = props.size; //typeof w is "medium" | "large"
    }

    if (props.variant === "pill") {
        const w = props.size; //typeof w is "small" | "medium" | "large"
    }
}

// allowed
Button({ variant: "hero", size: "medium" });

// not allowed
Button({ variant: "hero", size: "small" });

Note that we don't destructure the variant, doing so loses you the control flow analysis, https://github.com/microsoft/TypeScript/issues/9998

edit - here is a playground link https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4BYAKFEljgG9FiYBRAGyRCQDt4BfONlxwAREXQwRFSlRgBPMEjgARYAGc0AVW7AI3OAF4qAHwZwAbiijAUvAFyiAFkhxS4a4AC8kAfgciuABNgAFcQEThTETYrAHMkCL4qOBTIs0trWxh-MGA2NjcPbz9RNRAUfIiooNDwtOi4hLg+aSpMEO4MXX0ACRcIACEQmBg9AAowHDA1B1s5AEoHZAl2Th54emTUohgQqH0AHmDzAD5HYAOAemOTqiSZSnbOmG64AAU8tiGR8cmIadm3AWS2Yqy4vAYWxSOz2hxuACM5Egrjc7lQqEgAB60eCBJCYFAhNhYDpdPRwb6jbgTKYzFTqLQ6PSLJgrDjgjZQuBoPRqeCeQxwP7TAB0GRsvC5agA7sAYGhHHAaf81GKrBKYPNIZRUrruSg1EoRM5XA5Njq9Zaedw+XBpYLhaqikhSHBLpd5IoIJg7XB1KIamEqqIYlB4iIuZboUhdvs4Ac+jhKeT6CK046BJdbhbLfco-rDaJcpVtfnddbbfaDELaSLna73Z6kN7ff6RGUKgV6oG6lFQ+HI1GYXGDh98sn9Kn07TM9n83nUvd7ubdQcJxZ1VkDMb+hFnduexEs2jKCvUmvhlSN5leNuTRA914kNuOyXj5R7kA

Related