TypeScript Patterns for Enforcing Exactly One Required Field
When building real‑world applications, it is common to design APIs or internal utilities that accept an object where exactly one property must be provided. This pattern appears in routing, filtering, messaging systems, data loaders, analytics events, and various “one‑of” configurations.
Although the problem sounds simple, enforcing this constraint at the type level—without runtime checks—requires a deeper understanding of TypeScript’s advanced type mechanics.
This article revisits the concept, expands on the original solution, and provides improved examples, better explanations, and modern patterns suitable for large projects and strongly typed APIs.
Why This Pattern Matters
Consider an API that accepts a request to fetch messages. A message may belong to a project, a task, or an account. The business logic requires selecting exactly one:
-
projectId -
taskId -
accountId
Developers must not pass more than one at the same time, and omitting all of them is also invalid.
A naïve type definition might look like this:
type FetchMessages = {
projectId?: string;
taskId?: string;
accountId?: string;
};Despite its simplicity, it allows invalid combinations:
{ projectId: "p1", taskId: "t1" } // invalid but allowed
{} // invalid but allowedSolving this purely through TypeScript types is possible and eliminates entire classes of bugs.
A Natural but Insufficient Attempt
Many developers first reach for a union:
type FetchMessages =
| { projectId: string }
| { taskId: string }
| { accountId: string };This works—until you want optional metadata:
| { projectId: string; limit?: number }
| { taskId: string; limit?: number }Or shared fields:
| { projectId: string; offset?: number; includeHidden?: boolean }Copy‑pasting every shared field into each union member quickly becomes unmaintainable.
A reusable, strongly typed, automatic solution is far more suitable.
A General Type: Exactly One Key Required
We can construct a reusable helper type that enforces:
- exactly one property from a union of keys
- all other properties must not be present
- shared optional metadata remains compatible
Step 1: Create an exclusive‑choice type
type RequireExactlyOne<T, K extends keyof T = keyof T> =
K extends keyof T
? { [P in K]-?: T[P] } & { [Q in Exclude<K, P>]?: never }
: never;This type uses distributive conditionals to generate:
- a version for each key where that key is required
- all other keys become
never, preventing usage
Step 2: Apply It to the Model
type MessageSelector = {
projectId?: string;
taskId?: string;
accountId?: string;
};
type OneMessageSelector = RequireExactlyOne<MessageSelector>;Step 3: Test the Result
const validA: OneMessageSelector = { projectId: "p1" };
const validB: OneMessageSelector = { taskId: "t1" };
const validC: OneMessageSelector = { accountId: "u1" };Invalid examples:
const invalidA: OneMessageSelector = {};
// Error: one field required
const invalidB: OneMessageSelector = { projectId: "p1", taskId: "t1" };
// Error: mutually exclusive
const invalidC: OneMessageSelector = { taskId: undefined };
// Error: undefined is not allowedMaking It More Reusable
Often the set of required‑exactly‑one fields is smaller than the entire object. Consider:
type QueryOptions = {
projectId?: string;
taskId?: string;
accountId?: string;
includeHidden?: boolean;
page?: number;
};We want only the selector to be exclusive, while metadata remains optional.
This pattern separates them cleanly:
type SelectorKeys = "projectId" | "taskId" | "accountId";
type ExclusiveSelector<T> = RequireExactlyOne<Pick<T, SelectorKeys>> &
Omit<T, SelectorKeys>;Example:
type Query = ExclusiveSelector<QueryOptions>;Now:
const q1: Query = { projectId: "x1", includeHidden: true };
const q2: Query = { accountId: "abc", page: 5 };And invalid combinations still fail.
A Refined Version Using Mapped Types
The original article showed the following pattern:
type SelectOneField<T, K extends keyof T> = Record<K, Exclude<T[K], undefined>>;
type SelectOnlyOne<T> = {
[K in keyof T]-?: SelectOneField<T, K>;
}[keyof T];This works reasonably well but lacks flexibility and does not prevent additional properties.
A safer rewrite prevents extra keys and remains composable:
type ChooseOnlyOne<T> = {
[K in keyof T]: { [P in K]-?: NonNullable<T[P]> } &
{ [Q in Exclude<keyof T, K>]?: never };
}[keyof T];Usage:
type Exclusive = ChooseOnlyOne<{
projectId?: string;
taskId?: string;
accountId?: string;
}>;Practical Examples
Example: Analytics Event
Only one of three tracking identifiers may be used:
type TrackEventId = ChooseOnlyOne<{
adId?: string;
pageId?: string;
productId?: string;
}>;
function trackEvent(id: TrackEventId) { /* ... */ }Examples:
trackEvent({ adId: "A1" }); // valid
trackEvent({ pageId: "home" }); // valid
trackEvent({ productId: "P1" }); // valid
trackEvent({ adId: "A1", pageId: "home" }); // errorExample: Fetching Shared Resources
type LoadResource = {
projectId?: string;
taskId?: string;
version?: number;
};
type LoadRequest = ExclusiveSelector<LoadResource>;Runtime Validation (Optional but Useful)
Even with strong types, runtime validation is often necessary when accepting untyped external input.
A small helper ensures correctness at runtime as well:
function assertExactlyOne<T extends object>(
obj: T,
keys: (keyof T)[]
) {
const count = keys.filter(k => obj[k] !== undefined).length;
if (count !== 1) {
throw new Error("Exactly one of the fields must be provided.");
}
}Usage:
assertExactlyOne(payload, ["projectId", "taskId", "accountId"]);Final Thoughts
TypeScript gives developers powerful tools for enforcing structural correctness without cluttering code with checks. The “exactly one property must be set” pattern appears in many domains, and a reusable helper type such as RequireExactlyOne or ChooseOnlyOne can dramatically improve reliability across a codebase.
Whether you are designing API contracts, data selectors, routing utilities, or analytics events, enforcing mutual exclusivity at the type level reduces ambiguity and hides entire classes of bugs before they reach production.
By leaning on the type system, code becomes more expressive, safer, and easier to maintain.