I have key-value pairs that 'come in pairs'. In other words, if I have one key-value pair A:B then I also want to require my object to have C:D, but it can also have neither pair. (Think message:string for one pair and min-length:number for the other pair.) Is there an elegant way to make an interface made up of an arbitrary number of such pairs?
Edit: For clarification, I want to design an interface for objects where these would be allowed:
{
//First pair
message1: string;
minLength1: number;
//Second pair
message2: string;
minLength2: number;
// ...
}
{
//First pair omitted altogether
//Just the second pair
message2: string;
minLength2: number;
}
... but where objects like the following are NOT allowed because you only have one half of a pair:
{
//First pair
message1: string;
// minLength1: number; // Error †
// ...
}
† Error: if message1 is included then you must also include minLength1
For a given pair of properties, the closest approximation you can make to requiring both properties or neither is a union of the type with both properties and a type with optional properties of type never (which prevents the properties from being present with a value other than undefined). I got the idea from this answer. This will give you an error in the right cases, though the error message may not be particularly helpful. Then you can just intersect the unions for all the pairs of properties you want (as long as you don't have more than about 10 pairs, because when TypeScript simplifies a type, it distributes intersections of unions to make unions of intersections, which will blow up the size of the type exponentially). For your example:
type AllOrNone<T> = T | {[K in keyof T]?: never};
type MyType = AllOrNone<{
message1: string;
minLength1: number;
}> & AllOrNone<{
message2: string;
minLength2: number;
}>;
If you're really bothered by the undefined case and are willing to use a generic function to validate your objects instead of writing down a single type, then you can do something like this:
type CheckAllOrNone<A, T> =
T | (keyof A & keyof T extends never ? {} : never);
type CheckMyType<A> = CheckAllOrNone<A, {
message1: string;
minLength1: number;
}> & CheckAllOrNone<A, {
message2: string;
minLength2: number;
}>;
function checkMyType<A extends CheckMyType<A>>(arg: A) {
return arg;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With