Reputation: 934
I have two interfaces and want to create a function where either interface A or interface B is included but not both at the same time. Using a simple UnionType does not work as it also allows for both interfaces to be included
Example (https://stackblitz.com/edit/typescript-qgytsy)
interface A{
a: string;
}
interface B{
b: string;
}
function aOrB(modifier: A | B) {}
aOrB({ a: '' }); // works!
aOrB({ b: '' }); // works!
aOrB({ a: '', b: '' }); // also works but should not work!
Is there a way to achieve this, so that the first two calls to aOrB(...)
work but the last one not?
Br, Benedikt
Upvotes: 3
Views: 759
Reputation: 20132
TS type system is structural, that means {a: 'a', b: 'b'}
is proper member of A | B
because it is assignable to A
as it has all A
properties and assignable to B
because same reason. There is no issue to use such object where A
or B
is required as it has all what both need in terms of structure. Consider following snippet:
function fOnA(modifier: A) { } // we require only A
const a = { a: '', b: '' } // we create object with additional props
fOnA(a) // no error
We can do type check of type { a: '', b: '' }
.
type ExtendsA = { a: '', b: '' } extends A ? true : false // evaluates true
type ExtendsB = { a: '', b: '' } extends B ? true : false // evaluates true
As you can see { a: '', b: '' }
is valid object to be used for A
and for B
.
To be clear using such construct with additional properties is no harm, as we always have what we need.
The case when we want to distinct if we have A
or B
is a case where we should create discriminant in order to be able to check what kind of object went inside the function.
Consider:
interface A{
kind: 'A', // discriminant
a: string;
}
interface B{
kind: 'B', // discriminant
b: string;
}
function aOrB(modifier: A | B) {}
aOrB({ kind: 'A', a: '' }); // works!
aOrB({ kind: 'B', b: '' }); // works!
aOrB({ kind: 'B', a: '' }); // error as it should be
aOrB({ kind: 'B', b: '', a: '' }); // error as it should be
I have added kind
in order to distinguish both versions. Such approach has many benefits, as we can now easily check if we have A
or B
simply by checking the kind
.
Another solution is to block properties we don't want by never
keyword. Consider:
interface A{
a: string;
b?: never;
}
interface B{
b: string;
a?: never;
}
function aOrB(modifier: A | B) {}
aOrB({ a: '' }); // works!
aOrB({ b: '' }); // works!
aOrB({ a: '', b: ''}); // error as it should be
By adding prop?: never;
I avoid allowing object to pass A
and B
requirements.
Upvotes: 2
Reputation: 2009
You can define a type which excludes certain properties:
type IFoo = {
bar: string; can?: never
} | {
bar?: never; can: number
};
let val0: IFoo = { bar: "hello" } // OK only bar
let val1: IFoo = { can: 22 } // OK only can
let val2: IFoo = { bar: "hello", can: 22 } // Error foo and can
let val3: IFoo = { } // Error neither foo or can
Then use that type for the modifier parameter.
Upvotes: 3