Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Union Type missing properties

Say I have the following types:

type MessageType = 'example1' | 'example2' | 'example3'

type MessageHead = {
  +type: MessageType
}

type BaseBody = {
  +payload?: any,
  +data?: any
}

type LabelledBody = {
  +labelName: string
}

type MessageBody = BaseBody | LabelledBody

type Message = MessageHead & MessageBody

And I then consume a message like so:

[{name: 'example1'}, {name: 'potato'}].find(thing => thing.name === message.labelName)

Resulting in the following flow exception:

Cannot get message.labelName because:
 • all branches are incompatible:
    • Either property labelName is missing in MessageHead [1].
    • Or property labelName is missing in BaseBody [2].
 • ... 1 more error.

With type Message = MessageHead & MessageBody being displayed as the violated type

What I don't understand is why my Union Type doesn't allow for a message with a labelname?

Edit: Tryflow link:Tryflow link

like image 458
Abraham P Avatar asked Mar 18 '18 18:03

Abraham P


People also ask

What is union type in TypeScript?

TypeScript Union Type TypeScript allows a flexible type called any that can be assigned to a variable whose type is not specific. On the other hand, TypeScript allows you to combine specific types together as a union type. let answer: any; // any type. let typedAnswer: string | number; // union type.

What is TypeScript narrowing?

TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing.

Does not exist on type String []?

The "Property does not exist on type String" error occurs when we try to access a property that does not exist on the string type. To solve the error, use an object instead of a string, or make sure you're accessing a valid built-in method on the string.

Does not exist on type object TypeScript?

The "Property does not exist on type Object" error occurs when we try to access a property that is not contained in the object's type. To solve the error, type the object properties explicitly or use a type with variable key names.


1 Answers

Your union type does allow for a Message with a labelname. The problem is that it also allows for a Message without a label name. Consider the union you've got on this line:

type MessageBody = BaseBody | LabelledBody

This means that MessageBody can be either look like this:

type BaseBody = {
  +payload?: any,
  +data?: any
}

or

type LabelledBody = {
  +labelName: string
}

Taking this a step further, we can perform the intersection between MessageBody and MessageHead and see that the shape of Message can one of these two cases:

(case 1)

{
  +type: MessageType,  // From MessageHead
  +payload?: any,      // From BaseBody
  +data?: any          // From BaseBody
}

(case 2)

{
  +type: MessageType,  // From MessageHead
  +labelName: string   // From LabelledBody
}

So when Flow sees you're accessing the labelName of the message object, it believes (correctly), that the message object might look like case 1 (above). If we're in case 1, then you can't access the labelName, since it doesn't exist, so it throws an error. The simplest way to get around this is to create two types of messages, one with a labelName and one with the payload and data properties. Then you can annotate your function as receiving one of the types:

(Try)

type MessageWithLabel = MessageHead & LabelledBody

const exFunc = (message: MessageWithLabel) => {
  [{name: 'example1'}, {name: 'potato'}].find(thing => thing.name === message.labelName)
}

Alternatively, you can use a disjoint union to tell flow which case you're using. This strategy involves setting a property (e.g. type) which tells flow which type of object we're dealing with.

(Try)

type MessageWithBody = {|
  +type: 'base',
  +payload?: any,
  +data?: any
|}

type MessageWithLabel = {|
  +type: 'labelled',
  +labelName: string
|}

type Message = MessageWithBody | MessageWithLabel

const exFunc = (message: Message) => {
  if (message.type === 'labelled') {
    const labelName = message.labelName
    return [{name: 'example1'}, {name: 'potato'}].find(thing => thing.name === labelName)

    // Sidenote: 
    // I had to extract labelName to a constant for Flow to typecheck this correctly.
    // So if we used thing.name === message.labelName Flow will throw an Error.
    // If you don't extract it right away, flow thinks the Message may have changed
    // after pretty much any non-trivial action. Obviously it doesn't change in this
    // example, but, hey, Flow isn't perfect.
    // Reference: https://flow.org/en/docs/lang/refinements/#toc-refinement-invalidations
  } else {
     // Do something for the 'base' type of message
  }
}
like image 127
James Kraus Avatar answered Sep 28 '22 11:09

James Kraus