Type an options object where two fields must be both present or both absent
Write a generic helper AllOrNothing<T> that accepts either every key of T or none of them.
Requirements: passing all keys must type-check; passing an empty object must type-check; passing a strict non-empty subset must be a compile error. Do not add a runtime check, and do not enumerate the keys by hand — derive them from T.
type AllOrNothing<T> = /* your code here */;
type Range = AllOrNothing<{ start: Date; end: Date }>;
const a: Range = { start: new Date(), end: new Date() }; // ok
const b: Range = {}; // ok
const c: Range = { start: new Date() }; // must error
Write the implementation.
Union the full shape with a shape whose keys are all optional and typed never: type AllOrNothing<T> = T | { [K in keyof T]?: never }. The second half is a mapped type over keyof T, so it tracks T automatically. Supplying only start matches neither branch — it lacks end for the first and gives never a value for the second — so the compiler rejects it.
- ✗Assuming two optional fields already constrain each other — they do not
- ✗Intersecting the two halves instead of unioning them, which yields an unusable type
- ✗Giving up on the compiler and pushing the invariant into a runtime check
- →How would you extend this to an exclusive-or between two different shapes?
- →Why does an excess-property check matter for the empty-object branch here?
The "both or neither" invariant is expressed as a union of two shapes: the full one, and an "empty" one whose keys are all optional and typed never. No value of type never can be produced, so the second branch admits only an object without those keys.
type AllOrNothing<T> = T | { [K in keyof T]?: never };
type Range = AllOrNothing<{ start: Date; end: Date }>;
const a: Range = { start: new Date(), end: new Date() }; // ok — the T branch
const b: Range = {}; // ok — the never branch
// @ts-expect-error — end is missing, and start cannot be never
const c: Range = { start: new Date() };
The second branch's keys come from keyof T, so adding a field to the source shape widens the constraint on its own — there is nothing to keep in sync by hand.