typescriptunion-types

Enhance types autocompletion in Types Union


I have a function with parameters of the union type of two interfaces as the following:

interface LablededBadge {
    label: string
}

interface IconBadge {
    icon: string
}


export const Badge = (props: LablededBadge | IconBadge) => {
    return "Badge"
}


Badge({icon: "icon", label: "label"})

How can I enhance the type suggestion so that, passing icon would prevent the TS compiler from suggesting label? Is it doable in TS?


Solution

  • Union types in TypeScript are inclusive (so a value can be of type LabeledBadge | IconBadge if it is a value of type LabeledBadge or IconBadge or both) and not exclusive.

    And object types are open (so a value is of type LabeledBadge as long as it has all the required properties of LabeledBadge, but it may also have extra properties such as icon) and not exact.

    Putting that together it means that {label: "label", icon: "icon"} is both of type LabeledBadge and of type IconBadge, and therefore it will be accepted by the union.

    TypeScript doesn't have a perfect way to prohibit a certain property; the closest you can get is to make the property optional and of the impossible never type, like {icon: string; label?: never} | {label: string; icon?: never}, so that you can't actually use a defined value for the property. That's more or less the same as prohibiting it (give or take undefined).

    See Why does A | B allow a combination of both, and how can I prevent it? for a more complete discussion of this issue.

    Unfortunately that approach will still suggest the bad key to you in your IntelliSense-enabled IDE, if only to prompt you to make it of type undefined or never. If you want to completely prevent such suggestions, you'll need to change your approach entirely.


    The only way I can think to do this is to make your function an overload with two distinct call signatures, neither one of which involves a union. You'll still want your implementation to accept a union, but that call signature is not visible to callers:

    function Badge(props: LabeledBadge): void;
    function Badge(props: IconBadge): void;
    function Badge(props: LabeledBadge | IconBadge) {
      return "Badge"
    }
    

    And now when you call it, you'll see two distinct ways to do it:

    // Badge(
    //   ----^ 
    // 1/2 Badge(props: LabeledBadge): void
    // 2/2 Badge(props: IconBadge): void
    
    Badge({ label: "label" }); // okay   
    Badge({ icon: "icon" }); // okay
    Badge({ icon: "icon", label: "label" }); // error
    

    And furthermore, when you've already entered one of the keys, the compiler will resolve the call to that overload which has no reason to suggest the other key:

    Badge({icon: "",  })
    // ------------> ^
    // no suggested keys (or a bunch of general suggestions)
    

    Playground link to code