TypeScript Require One of Two Properties
Understanding TypeScript Interfaces
- They help enforce type safety and code consistency.
- They specify the properties and their types that an object must have.
- Interfaces define the contract or shape of an object.
Requiring One of Two Properties
- To achieve this, you can use a combination of optional properties and conditional types.
Steps
-
Define Optional Properties
- Create an interface with the properties you want to make optional.
- Use a question mark (
?
) after the property name to indicate that it's optional.
interface MyInterface { propertyA?: string; propertyB?: number; }
-
Create a Conditional Type
- Define a conditional type that checks if either
propertyA
orpropertyB
exists. - Use a type guard to check for the presence of each property.
type RequireOneOf<T, K extends keyof T> = T extends Pick<T, K> ? T : never;
- Define a conditional type that checks if either
-
Apply the Conditional Type
- Use the conditional type to create a new interface that requires one of the optional properties.
interface MyRequiredInterface extends RequireOneOf<MyInterface, 'propertyA' | 'propertyB'> {}
Explanation
- Otherwise, the conditional type returns
never
, indicating that the interface doesn't meet the requirement. - If
T
extendsPick<T, K>
, it means at least one of the properties inK
is present, and the conditional type returnsT
. - The conditional type checks if the interface
T
extendsPick<T, K>
.Pick<T, K>
extracts only the properties specified inK
fromT
.
RequireOneOf<T, K extends keyof T>
takes two parameters:T
: The interface you want to enforce the requirement on.K
: A union of the property keys you want to make mandatory.
Example Usage
const obj1: MyRequiredInterface = { propertyA: 'value' }; // Valid
const obj2: MyRequiredInterface = { propertyB: 123 }; // Valid
const obj3: MyRequiredInterface = {}; // Invalid, both properties are missing
interface MyInterface {
propertyA?: string;
propertyB?: number;
}
type RequireOneOf<T, K extends keyof T> = T extends Pick<T, K> ? T : never;
interface MyRequiredInterface extends RequireOneOf<MyInterface, 'propertyA' | 'propertyB'> {}
Breakdown
-
- The conditional type uses
Pick<T, K>
to extract only the properties specified inK
fromT
.
- The conditional type uses
-
- The
MyRequiredInterface
interface extendsRequireOneOf<MyInterface, 'propertyA' | 'propertyB'>
. - This ensures that any object conforming to
MyRequiredInterface
must have eitherpropertyA
orpropertyB
(or both) defined.
- The
const obj1: MyRequiredInterface = { propertyA: 'value' }; // Valid
const obj2: MyRequiredInterface = { propertyB: 123 }; // Valid
const obj3: MyRequiredInterface = {}; // Invalid, both properties are missing
obj3
is invalid because it doesn't have eitherpropertyA
orpropertyB
.obj1
andobj2
are valid because they have at least one of the required properties.
Key Points
- The
RequireOneOf
type is a reusable utility that can be applied to various scenarios where you need to require one or more properties in an interface. - By using this approach, you can enforce strict type checks and ensure that objects of a specific interface adhere to the specified property requirements.
Alternative Methods for Requiring One of Two Properties in TypeScript Interfaces
While the conditional type approach is a common and effective method, there are a few other alternatives you can consider:
Intersection Types
- Example
- Concept
Create an intersection type that combines two interfaces, each with one of the required properties.
interface PropertyAInterface {
propertyA: string;
}
interface PropertyBInterface {
propertyB: number;
}
type MyRequiredInterface = PropertyAInterface & PropertyBInterface;
Discriminant Unions
- Concept
Use a discriminant property to differentiate between objects of different types within a union.
type PropertyType = 'propertyA' | 'propertyB';
interface PropertyAInterface {
type: PropertyType;
propertyA: string;
}
interface PropertyBInterface {
type: PropertyType;
propertyB: number;
}
type MyRequiredInterface = PropertyAInterface | PropertyBInterface;
Custom Type Guards
- Concept
Create custom type guards to check for the presence of specific properties.
function isPropertyAInterface(obj: any): obj is PropertyAInterface {
return 'propertyA' in obj;
}
function isPropertyBInterface(obj: any): obj is PropertyBInterface {
return 'propertyB' in obj;
}
Optional Chaining and Nullish Coalescing
- Concept
Use optional chaining (?.
) to safely access properties and nullish coalescing (??
) to provide default values.
interface MyInterface {
propertyA?: string;
propertyB?: number;
}
function requireOneOf(obj: MyInterface): string | number {
return obj.propertyA ?? obj.propertyB;
}
Choosing the Right Method
- Optional chaining and nullish coalescing
Can be used for more complex scenarios, but might require additional logic. - Custom type guards
Provide fine-grained control over type checking. - Discriminant unions
Are useful when you need to differentiate between objects based on a common property. - Intersection types
Work well when you have clear-cut definitions for each property type. - Conditional types
Are generally the most flexible and concise approach.
typescript