How to Choose Exactly One Mandatory Field in an Object
When working with TypeScript, you might encounter scenarios where you need to ensure that exactly one field from an object is selected, and this field must be mandatory. This often happens in situations such as API requests, where you want to restrict the input to a single option.
In this article, we'll explore how to implement this using TypeScript's type system.
Problem Statement
Imagine you have an object representing a request that allows you to choose between projectId
, taskId
, or userAccountId
, but only one of these fields should be selected. The initial TypeScript type might look like this:
1 type FetchMessages = {2 projectId?: string;3 taskId?: string;4 userAccountId?: string;5 };
The goal is to create a type that:
- Ensures exactly one of these fields is selected.
- Ensures none of the other fields are included.
- Utilizes TypeScript’s type system to avoid manual validation.
Solution Steps
1. Single Field Extraction:
Start by defining a helper type that extracts exactly one field and removes undefined values:
1 type SelectOneField<T, K extends keyof T> = Record<K, Exclude<T[K], undefined>>;
This type ensures that only one key (projectId
, taskId
, or userAccountId
) is mandatory.
2. Combining Multiple Fields:
To ensure this works for all three fields (projectId
, taskId
, userAccountId
), map over each key and combine them:
1 type SelectOnlyOne<T> = {2 [K in keyof T]: SelectOneField<T, K>;3 }[keyof T];
3.Ensuring Non-Optional:
By default, TypeScript's mapped types can make the result optional. To make sure the result is mandatory, remove the ?:
1 type SelectOnlyOne<T> = {2 [K in keyof T]-?: SelectOneField<T, K>;3 }[keyof T];
This results in a type where exactly one of the fields (projectId
, taskId
, or userAccountId
) must be selected.
Example Usage
1 type FetchMessages = {2 projectId?: string;3 taskId?: string;4 userAccountId?: string;5 };67 // Ensures exactly one field is selected and non-optional8 type TestMessages = SelectOnlyOne<FetchMessages>;910 // Valid examples:11 const example1: TestMessages = { projectId: 'p1' }; // ✅ valid12 const example2: TestMessages = { taskId: 't1' }; // ✅ valid13 const example3: TestMessages = { userAccountId: 'u1' }; // ✅ valid1415 // Invalid example: more than one field selected16 const example4: TestMessages = { projectId: 'p1', taskId: 't1' }; // ❌ invalid
Benefits of This Approach:
- Type Safety: Ensures only one field is selected, with TypeScript catching invalid cases.
- Error Prevention: Eliminates the need for manual runtime validation.
- Cleaner Code: Using auto-generated types simplifies your solution and makes it easier to maintain.
Conclusion
By leveraging TypeScript's type system, you can create robust and safe types that enforce exactly one field to be selected in an object. This approach improves the clarity, safety, and maintainability of your code.