typescript Feb 19, 2026
XOR Types for Mutually Exclusive Options
Enforce exactly one of two shapes at compile time.
Use XOR when two option shapes are mutually exclusive but share no discriminant field. A discriminated union is usually better; XOR covers the cases where you can’t add a type or kind property to distinguish the branches.
Type
xor.ts
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type XOR<T, U> = (T & Without<U, T>) | (U & Without<T, U>)
type ById = { id: string }
type ByEmail = { email: string }
type Lookup = XOR<ById, ByEmail>
function getUser(input: Lookup) {
return input
}
getUser({ id: 'u1' })
getUser({ email: 'a@b.com' })
// @ts-expect-error: both provided
getUser({ id: 'u1', email: 'a@b.com' }) How it works
Without<T, U>takes every key inTthat doesn’t exist inUand maps it to?: never. This makes those keys “forbidden” on any object that matchesU.XOR<T, U>is a union of two branches. The first branch allowsT’s keys and forbidsU’s extras; the second allowsU’s keys and forbidsT’s extras. Exactly one branch can match at a time.- The
@ts-expect-errorline proves it: passing bothidandemailis a compile error because neither branch permits both keys simultaneously.
When to prefer a discriminated union
If you control the shapes, a discriminant field (type: "byId" | "byEmail") is clearer and plays better with switch/if narrowing. XOR is for situations where you inherit the shapes from an external API or a library type you can’t modify.