Pitfalls

Both reflex and flow are fairly new tools and there are certain issues users may run into while using them. This document lists some of the common pitfalls that users may run into when following common pattern. Our hope is to steer users away from running into issues, until they get fixed.

Provide default in switch (until #451 is fixed)

At the moment of writing flow's pattern matching is not exhaustive (see #451). In plain English this means that if default case is not present in switch statement flow will fail with confusing error. Here is an example where one most likely run into this:

export const update = (model:Model, message:Message):Model => {
  switch (message.type) {
    case "Increment":
      return increment(model)
    case "Decrement":
      return decrement(model)
  }
}

Until #451 is fixed it is recommended to resort to resort to either runtime or a logged error in default case. Runtime errors are easier to catch due to support from debuggers, but often can leave application in a broken state. Logged errors never leave application in a broken state but make errors less apparent and more difficult to catch. So here is our workaround:

Runtime Error

export const update = (model:Model, message:Message):Model => {
  switch (message.type) {
    case "Increment":
      return increment(model)
    case "Decrement":
      return decrement(model)
    default:
      return panic(model, message)
  }
}

export const panic = <model, message> (model:model, message:message) => {
  if (window.throwInPanic) {
    throw TypeError(`Unsupported message received ${message}`)
  } else {
    console.error('Unsupported message received', message)
    return model
  }
}

In general use of default case when refining a message type is not recommended as that reduces set of guarantees that type checker can provide. For example it is not able to tell if you update handles all supported message or not, which is crucial when more messages are introduced. We highly recommend to remove default cases once flow issue #451 is fixed.

Avoid same named fields in tagged unions

At the moment of writing flow breaks in subtle ways when types in the union have multiple fields with a same name. Below is an example where time is an offending field found in Tick and Load messages:

type Message =
  | { type: "Click" }
  | { type: "Tick", time: number }
  | { type: "Load", time: number, url: string }

Note: The issue does not manifest itself until type unions across modules are used. This means new code using above Message will not type check even if everything is sound. Flow error messages will be confusing & it will be hard to identify the problem, as it is in existing code that used to type check.

Recommended workaround is to have one common field across types in union a.k.a sentinel field for type refinement (Field named type in this case). Have a second unique field for data when necessary.

type Message =
  | { type: "Click" }
  | { type: "Tick", tick: number }
  | { type: "Load", load: { time: number, url: string } }

Note: We recommend value of the first field (in this example type) as name for the second field as sentinel fields are guaranteed to be unique across the union.

Please note that for Load message we intentionally moved time and url fields under load. It makes it easier to avoid this general issue over time, as more message types with url field may be added.

results matching ""

    No results matching ""