ts-xor
A generic TypeScript utility type that composes object types with mutually exclusive keys.
Repository Health
Technical Analysis
ts-xor is a zero-dependency TypeScript utility library that implements a generic XOR mapped type, letting you compose object types whose keys are mutually exclusive rather than simply unioned. Where TypeScript’s built-in | operator only produces a superset type that can contain keys from both sides, XOR<A, B> produces a type where exactly one side’s keys can be set at a time — closing a gap TypeScript’s own union operator has never addressed.
The library supports XOR-ing between two and up to 200 generic type parameters, and its own source is partly generated rather than hand-written: the core XOR type declaration is produced programmatically using the TypeScript Compiler API, letting the generated union scale to that parameter count without maintaining hundreds of near-duplicate overloads by hand.
What You Get
- A generic
XOR<A, B, ...>type supporting 2 up to 200 mutually exclusive type parameters in one expression - A zero-runtime-dependency, types-only package — no JavaScript logic is emitted or executed
- Full type declarations (
.d.tsand.d.mts) for both CommonJS and ESM consumers via dual package exports - Supporting helper types (
Without,Prettify,EvalIfNotUnknown) that compose the XOR implementation and remain independently useful
Common Use Cases
- API responses with data-or-error shapes - typing a fetch result as
XOR<{ data: P }, { error: FetchError<P> }>so consuming code gets compile-time narrowing ondatavserror. - Modeling mutually exclusive API payload variants - typing a response where a
rainorsnowkey (but never both) is present, with nested accuracy fields like1hvs3hXOR-ed the same way. - Enforcing config objects with alternate option sets - preventing callers from setting both members of a conflicting configuration pair by encoding the exclusivity directly in the type instead of relying on manual runtime validation.
- Replacing ad hoc union-type workarounds - teams that previously hand-rolled
Omit/nevertricks per type pair can standardize on one genericXORutility across a codebase.
Under The Hood
Architecture
This is a tiny, single-purpose type library with no runtime code: src/index.ts simply re-exports everything from src/types/xor.ts. Unusually, that core XOR type alias is not hand-written — it is generated by src/xorFactory.js, a Node build script that walks the TypeScript Compiler API’s factory functions (ts.factory.*) to construct the AST for a type alias supporting up to 200 generic parameters, then prints the result to src/types/xor.ts via the codegen script wired into the prebuild/pretest npm hooks. The generated type composes three small, independently hand-written helper types — Without (strips the keys of one type from another), EvalIfNotUnknown (short-circuits unused optional parameters so extra XOR slots don’t pollute the union), and Prettify (flattens the mapped type so editors show resolved keys instead of the raw expression) — into a single recursive union.
Tech Stack
The package has zero runtime dependencies; its only devDependencies are typescript (^5.2.2), tsup (^7.2.0) for bundling, and publint (^0.2.2) for validating the published package layout. tsup builds src/index.ts into both ESM (dist/index.mjs) and CJS (dist/index.js) output with matching .d.ts/.d.mts declarations and source maps. tsconfig.json targets esnext under strict mode with Node16 module resolution. Continuous integration runs via a .travis.yml config that installs dependencies and runs the test suite plus build on every push.
Code Quality
Rather than a conventional runtime test framework, correctness is verified entirely at the type level: dozens of *.spec.ts fixture files under test/, grouped into scenario folders (two-hundred-xored-types, four-xored-types, shared-and-xored-members, single-member-objects, control-std-union-without-xor), assert structural typing behavior by deliberately assigning valid and invalid shapes. scripts/run-tests.sh loops over every spec file and runs tsc --noEmit on each, failing the suite if any file fails to compile as expected. publint additionally checks that the dual ESM/CJS package exports are actually consumable. strict: true is enabled throughout, and the handful of source files carry consistent JSDoc comments explaining intent.
API Design
The public surface is a single generic type import with no configuration and no runtime cost (sideEffects: false, nothing to tree-shake because nothing is emitted). Most consumers only ever pass two type arguments, but scaling to more follows the exact same syntax up to 200. The README documents the type with a truth table, VS Code hover screenshots, and two fully worked real-world patterns (data-or-error, weather-forecast payload), keeping the getting-started boilerplate to one import line. The most distinctive technical choice is generating the many-parameter variant with the TypeScript Compiler API instead of hand-authoring or macro-based tricks — an approach that’s uncommon among comparable “type gymnastics” utility packages, even though the underlying XOR-via-Without-and-intersection technique itself is a known pattern in the TypeScript community.