Skip to main content

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 | BA & 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.