Bene
Bene

Reputation: 934

Typescript - Pass one element of UnionType to function but not both

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

Answers (2)

Maciej Sikora
Maciej Sikora

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

Romi Halasz
Romi Halasz

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

Related Questions