utility-types
utility-types copied to clipboard
Mutually exclusive group
Is your feature request related to a real problem or use-case?
It'd be a good idea to be able to create mutually exclusive groups for options that cannot coexist.
Use cases:
Many APIs have filters of which exactly one can be used at a time. So to define their schemas, such a feature can prove to be really helpful. Also, there are many options/flags which cannot be used simultaneously in one command line.
Describe a solution including usage in code example
Possible code example: Say, we have 2 properties declared in a mutually exclusive group in an interface. Now, exactly one of those properties can exist, with no two properties coexisting simultaneously in an object having type of that interface.
interface args {
(arg1: string | arg2: string);
}
Additionally, we can also think of making the entire group optional, maybe with something like this:
interface args {
?(arg1: string | arg2: string);
}
Example:
interface args {
(arg1: string | arg2: string);
}
Now, if this interface is used as a function parameter:
function command(params:args) {
//do something
}
let obj: args = {arg1: "foo", arg2: "bar"};
command(obj);
The following code snippet should throw an error, since the two properties arg1 and arg2 cannot coexist in the same object.
Who does this impact? Who is this for?
Many people using TypeScript must have felt the need for declaring mutually exclusive groups.
This would be really nice to have it included in utility-types
library!
It is possible to define such shape using discriminated union type, but it requires having one shared property to infer correct type. That is acceptable approach for many cases, where some properties would be shared anyway.
interface TextInput {
type: "text";
value: string;
}
interface NumberInput {
type: "number";
value: number;
}
function Input(props: TextInput | NumberInput) {}
But then there are cases where some properties should be mutually exclusive, without the necessity to use additional "type"
property for proper type inference.
interface ImageFile {
/** Local file URL is resolved internally. */
file: File;
}
interface ImageSrc {
src: string;
}
function Image(props: ImageFile | ImageSrc) {}
We want to say that Image
can have either file
or src
prop, but actually the following code is still valid, because TypeScript does not treat unions as exclusive by default.
Image({ file: foo, src: "bar" }); // no error
The solution is to extend our interfaces to also describe properties which should not be present, defining them as optional value of undefined
(or never
) type.
interface ImageFileX {
/** Local file URL is resolved internally. */
file: File;
src?: undefined;
}
interface ImageSrcX {
src: string;
file?: undefined;
}
function ImageX(props: ImageFileX | ImageSrcX) {}
Now it works as expected.
ImageX({ file: foo }); // ok
ImageX({ src: "bar" }); // ok
ImageX({ file: foo, src: "bar" }); // error
But there is a lot of boilerplate which does not really provide any additional information and I think it actually makes the code less readable, especially when you have multiple related properties bound together...
interface BaseInput<T> {
name?: string;
value: T;
error: boolean;
onChange: (next: T) => void;
}
interface ManagedInput<T> {
name: string;
value?: never;
error?: never;
onChange?: never;
}
There seems to be quite long discussion in related issue presenting some solutions, so it might be worth exploring...