I'm working on a usecase where I need to enforce a set of object keys based on certain conditions meaning i have an object type with some properties and I want to define a utility type that can conditionally enforce the presence of these properties based on the values of other properties
Here is the type definition:
export type User = {
id: string;
name: string;
email?: string;
phoneNumber?: string;
contactPreference: 'email' | 'phone';
};
Now i want to create a type ConditionalRequired
which makes sure that
contactPreference
is email
, then email
is requiredcontactPreference
is phone
, then phone
is requiredExample:
// happy flow
const user1: ConditionalRequired<User> = {
id: '1',
name: 'foo',
contactPreference: 'email',
email: '[email protected]', // here email is required
};
// Here it should cause type error because email is missing
const user2: ConditionalRequired<User> = {
id: '2',
name: 'foo2',
contactPreference: 'email',
};
// Here it should cause type error because phone is missing
const user3: ConditionalRequired<User> = {
id: '3',
name: 'foo3',
contactPreference: 'phone',
};
This is my unsuccessful attempt to achieve this:
type ConditionalRequired<T> = T & {
[K in keyof T]: T[K] extends 'email' ? { email: string } :
T[K] extends 'phone' ? { phoneNumber: string } : {};
};
The use case you're describing would be best served by a union type and not a conditional type.
You want to say that your user objects should have either an "email"
contactPreference
and a required email
property, or a "phone"
contactPreference
and a required phoneNumber
property. That "either-or" relationship is what unions do.
In your case, we can represent the two possibilities as their own subtypes of User
:
interface EmailPreferredUser extends User {
contactPreference: 'email',
email: string;
}
interface PhonePreferredUser extends User {
contactPreference: 'phone',
phoneNumber: string;
}
And then your desired type is the union of them:
export type ConditionallyRequiredUser =
EmailPreferredUser | PhonePreferredUser
Note that I didn't write a ConditionalRequired<T>
utility type, since it's not clear that such a thing is useful. If the implementation needs to know about phone
and email
then it's not general enough to be reusable. Your example only used ConditionalRequired<User>
.
Okay, let's test it:
const user1: ConditionallyRequiredUser = {
id: '1',
name: 'foo',
contactPreference: 'email',
email: '[email protected]',
}; // okay
const user2: ConditionallyRequiredUser = { // error!
// ~~~~~ <-- Property 'email' is missing
id: '2',
name: 'foo2',
contactPreference: 'email',
};
const user3: ConditionallyRequiredUser = { // error!
// ~~~~~
// Property 'phoneNumber' is missing
id: '3',
name: 'foo3',
contactPreference: 'phone',
};
Looks good. It behaves as desired in both the good and bad cases.