loading...

Why does typescript not conditionally pick the right type in a union?

thibmaek profile image Thibault Maekelbergh ・1 min read

Why does the | (Union) operator not work as a logical or?

In the following snippets I would've expected it to see that the name property is available in 1 of the provided union options? Instead it errors that it can't find name because it is not available in MyDevice but it is available in the interface provided in the union.

How do you go about conditionally deciding to pick the interface instead of MyDevice when the name property is available?

I know conditional types exist but that doesn't seem like an option because I'm providing plain interfaces and not types/interfaces with generics.

img

Alt Text

Discussion

markdown guide
 

It's works exactly as union. Union says one or another let's say A | B so if you don't know yet which of these two your value represents you cannot use it's properties. Let's say A has property name but B has no such then TS is properly securing from the runtime error when you would want to use name on object without name.

In order to know if we have A or B at this point we need to set a type guard so generally some condition.

When you consider sets as types then by union let's say C you create a type with two objects A, B, the value of C can be either A or B never A and B in the same time.

If you want to have all properties of two objects then you want a product type of them which can be achieved by &, so A & B will create a type which considers all fields of both types.

 

OK, I realized it shortly after posting. However, how would you ever handle a typical case like a state reducer (in Redux for example) where your payloads can contain different subsets of data but they occur via events?

interface PayloadA {
  propA: string;
}

interface PayloadB {
  propB: number;
}

interface Action = {
  type: string;
  payload: PayloadA | PayloadB;
}

function reducer(state = {}, action: Action) {
  switch(action.type) {
    case 'ADD':
       return { state, action.payload.propA }
    case 'UPDATE':
      return { state, action.payload.propB }
    default: return state;
  }
}

TS always warns that propB would not be available in type ADD being dispatched and propA not being available in UPDATE being dispatched.

 

So the proper typying of this case is:

interface PayloadA {
  type: 'ADD';
  propA: string;
}

interface PayloadB {
  type: 'UPDATE';
  propB: number;
}

type Action = PayloadA | PayloadB 

We need to join type with prop, in other way TS is not able to understand what property is in add action or update action. Such construct is known as sum type or discriminated union.

 

This is completely normal - you have come into territory where TypeScript is trying to protect you from accessing potentially non-existing property.

If you take a look at documentation for union types you will find this:

If we have a value that has a union type, we can only access members that are common to all types in the union.

You need to refactor your solution to handle this case in a better way. Btw, why you don't use intersection types?