union
UnionToIntersection, XOR, and union key arithmetic.
import type {
UnionToIntersection,
ExcludeKeys, OverlappingKeys, Only, XOR,
} from '@gentleduck/ttest/union'
Set-theoretic operations on unions and on the keys of two object types. Pair with object for the broader key-arithmetic suite and discriminated when the union is tagged.
UnionToIntersection
type UnionToIntersection<U> =
(U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never
Convert a union to an intersection by exploiting the contravariant position of function parameters. A | B → A & B.
type a = UnionToIntersection<{ a: 1 } | { b: 2 }>
// ^? { a: 1 } & { b: 2 }
type b = UnionToIntersection<'x' | 'y'>
// ^? 'x' & 'y' (= never — string literals are disjoint)
// Combine handler shapes from a union of event objects.
type Handlers<U> = UnionToIntersection<U extends { kind: infer K extends string }
? { [P in K]: (e: U) => void }
: never>
Edge case: the helper consumes its input contravariantly. UnionToIntersection<any> is any and UnionToIntersection<never> is unknown — both are unhelpful, so check with IsAny / IsNever first.
ExcludeKeys
type ExcludeKeys<O, K extends keyof any> = Omit<O, Extract<keyof O, K>>
Remove keys K from object type O. Equivalent to Omit but the constraint on K is keyof any (any PropertyKey) rather than keyof O, so the helper does not error when K is wider than O's keys.
type a = ExcludeKeys<{ a: 1; b: 2; c: 3 }, 'b' | 'c'> // { a: 1 }
type b = ExcludeKeys<{ a: 1 }, 'missing'> // { a: 1 } (vs `StrictOmit`, which errors)
OverlappingKeys
type OverlappingKeys<A, B> = Extract<keyof A, keyof B>
Keys present in both A and B.
type a = OverlappingKeys<{ a: 1; b: 2 }, { b: 3; c: 4 }> // 'b'
type b = OverlappingKeys<{ a: 1 }, { b: 2 }> // never
Only
type Only<A, B> = { [K in Exclude<keyof A, keyof B>]: A[K] }
Properties of A whose keys are not in B. The set-difference building block for XOR.
type a = Only<{ a: 1; common: 2 }, { common: 3; b: 4 }>
// ^? { a: 1 }
XOR
type XOR<A, B> = Only<A, B> | Only<B, A>
Symmetric difference: keys exclusive to A or B, not both. Useful for "one shape or the other" payloads.
type a = XOR<{ a: number; common: string }, { b: boolean; common: string }>
// ^? { a: number } | { b: boolean }
// "Pass a URL or an inline file, never both."
type Source = XOR<{ url: string }, { contents: string }>
declare function load(src: Source): void
load({ url: '/x' }) // ok
load({ contents: 'hi' }) // ok
load({ url: '/x', contents: 'hi' }) // Error
Edge case: for objects that share no keys, XOR<A, B> collapses to A | B. Prefer MergeExclusive when you want a stricter "at most one shape" constraint that also rejects the empty-key overlap case.