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