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.