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?
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)