1

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?

Hazem Alabiad
  • 1,032
  • 1
  • 11
  • 24
  • 1
    If you really care about the keys suggested by the IDE then I think you need to make your function an overload *instead* of accepting a union, as shown [in this playground link](https://tsplay.dev/mMzlkW). The basic issue is the same as [this one](https://stackoverflow.com/q/46370222/2887218); unions are not exclusive. But the normal fix for that will still suggest the other key (but will complain if you actually use it). Does this fully address your question? If so I could write up an answer explaining; if not, what am I missing? – jcalz Apr 12 '23 at 18:16
  • Yes, this solved the issue :) This is actually a react component. https://stackblitz.com/edit/vitejs-vite-chf2ox?file=src/Badge.tsx Thanks in advance – Hazem Alabiad Apr 12 '23 at 18:50
  • Does this question suddenly depend on react? I'm in the middle of writing up my answer but I don't have any insight into the react part of this. Let me know if I should give up on my answer or proceed without mentioning react – jcalz Apr 12 '23 at 18:57
  • It is basically for React component but, proceed please with the TS solution I can pick it up from there :) – Hazem Alabiad Apr 12 '23 at 18:58

2 Answers2

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • How about the type guard? How can I access the props safely inside the function? – Hazem Alabiad Apr 12 '23 at 21:38
  • 1
    Followup questions probably should be brought up elsewhere; you can probably use the `in` operator to narrow (e.g., `if ("icon" in props)`) but further discussion of that isn’t in scope here. – jcalz Apr 12 '23 at 21:46
  • Thanks... no more follow-ups :) I already got the solution working, but, was looking for the best solutions. You did an amazing job, appreciate you! – Hazem Alabiad Apr 12 '23 at 21:56
0

This solution works:

    interface LablededBadge {
        label: string
    }
    
    interface IconBadge {
        icon: string
    }
    
    type LabeledOrIconBandge = LablededBadge & { icon?: never} | IconBadge & {label?: never}
    export const Badge = (props:  LabeledOrIconBandge) => {
      props
        return "Badge"
    }
    
    
    Badge({icon: "icon",}) 
    Badge({label: "label",})
    Badge({label: "label", icon: 'icon'}) // error
heaxyh
  • 573
  • 6
  • 20
  • 1
    Well no, it still suggests `label` when using `{ icon: "icon" }` and vice versa. – kelsny Apr 12 '23 at 18:18
  • It still suggests the other key (at least in my IDE). It's strictly better than the original version because at least you'll see that the value has to be `undefined`, but it's not exactly what the question is asking for, which is not to have the other key suggested at all. – jcalz Apr 12 '23 at 18:18
  • Well, in this [playground](https://www.typescriptlang.org/play?noUnusedParameters=true#code/ATCWDsBcFMCcDMCGBjawAyiBGAbaATAgIUXwHM0BvAKBDrp22hwC5gBnSWCM2ugXz4ghYKHCSpgASWQB7cCXJUR9UHPBtO3cL3qD6IyAE8ADmkxZmBAPKwZ8kuCXAAvBmx5C+RRWAAyYEowdQB+NnBoADc4fmAAH2l1HzQAykZLHDDgCOjYfTpoAA8TWVhIYHVOYGTXYAAKE1hZE3Y2dwybOyTEJwoASlcAPkCVYEbm9lGQWGhIAFdYcGAAImTlkXzhA3pkuso1eTZlg-BlgBp+AZFdtKZWFfTmc8vr0go9x-vlz-Pgw+AAOQnAGXYAAejBwDgTVgQA) it will generate an error. May you have different tsconfig settings. – heaxyh Apr 12 '23 at 18:20
  • @jcalz no the property won't be undefined because there is no declaration of this key in the object. – heaxyh Apr 12 '23 at 18:25
  • 1
    You have `{label: string, icon?: never} | {label?: never, icon: string}`. Both keys are declared in both sides of that union, so you will definitely be prompted for the other key, but the value type will be `undefined` (or possibly `never` if you have `--exactOptionalPropertyTypes`). I can reproduce this easily on the TS playground, as you can see [in this screenshot](https://imgur.com/2u7fEyl). – jcalz Apr 12 '23 at 18:34
  • @heaxyh I can confirm that it still suggests both, but, after typing both it is gonna tell you that it is an error. – Hazem Alabiad Apr 12 '23 at 18:47
  • @HazemAlabiad your problem was clearly solved. That your IDE doesn't support that kind of static analyst is another issue. – heaxyh Apr 13 '23 at 05:13