Why I was looking forward to Flow, and then I wasn’t

Flow has been on my radar for some time now as a viable type solution for new and existing Javascript projects. There are a handful of questions that are important to me that help me narrow down which type solution is worth diving deep into.

  • Is null a separate type?
  • Does the language have type aliases?
  • How good is the compiler at type inference?
  • Does the language have union types and tagged unions?
  • How is the JS interop experience?
  • Is there an “any” type? If so, how do I keep it under control?
  • How high is the learning curve?

There are dozens more questions I do ultimately end up seeking answers to, but the questions listed above serve as a useful initial filter for type solutions that deserve a good, hard look.

The candidates that pass this initial filter for me are Elm, Flow, and TypeScript. Elm is a non-starter for the particular project I was looking at so let’s quickly look at why I started with Flow over TypeScript.

Why I was looking at Flow instead of TypeScript

Overall, Flow and TypeScript fair pretty closely when answering those questions. Their syntax and approach is incredibly similar. However, Flow inched ahead of Typescript for a few reasons which is why I was initially optimistic and excited to look into Flow.

Null is a separate type in Flow

The idea that null is a separate type is fundamental to a good type system, in my opinion. TypeScript treats null as a subtype, which means that if you declare a variable as a number what that really means is it can be assigned to a number or null. Numbers can only be assigned to numbers in Flow.

As it turns out, TypeScript does have a “--strictNullChecks” flag which could invalidate this concern (assuming that flag is actually used). However, it does leave me wondering what other hindsight flags are yet to be discovered or implemented with TypeScript.

TypeScript reportedly has issues catching null

For someone who doesn’t have the time to exhaustively deep dive into every tech that pops up on my feed I heavily rely on content produced by others. One particular presentation demonstrated that TypeScript has some issues with null that Flow does not. Catching this “billion dollar mistake” is very high on my must-have list.

TypeScript is a self-described superset of Javascript

I’ll be honest, that tagline is not terribly appealing to me. There are some Javascript language features that could be better, and others that I would like to see removed. So when I see “superset of Javascript” I’m suspicious on whether that’s actually a good thing.

Admittedly, that’s a bit of a knee jerk reaction. All I can say is it’s a tagline that left me asking, “Why would I want that?”. Perhaps someone reading this can convince me why I would.

Like I said, on the initial pass Flow and TypeScript are incredibly similar. But TypeScript left me with questions. Are any of these concerns still issues? Are they on TypeScript’s roadmap to be fixed in the near future? I don’t know. But it was enough for me to say that if I only have X hours of free time to look into either Flow or TypeScript, I’m going to put those hours towards looking into Flow. The only real aspect of Flow that I saw lacking on its surface was the relatively smaller momentum and community compared to TypeScript.

The Good

So, I spent a weekend looking into Flow. I found some good, some meh, and some bad. First the good.

Type Aliases, Union Types, Tagged Unions, and Null as a separate type

Flow has all of these, which is great. I was in particular excited to see that Flow has the equivalent of Tagged Unions (referred to as a Disjoint Union in Flow) as they can be incredibly powerful when it comes to modeling your application state.

Gradual Typing

With Flow I can gradually convert my project file by file. This is a huge win because a “rip the bandaid, let’s do it all at once” approach just isn’t a practical strategy for existing production applications.

Strict Type Definitions

It’s possible to create a type alias that is strictly defined by using the exact object type syntax. Having this available is highly desirable since, after all, the point of type checking code is to ensure the data matches the types that I declared them as.

Community-Driven Library Definitions

Being able to use my third-party libraries is a must. I really don’t want to lose Ramda, or Redux, or the half a dozen other critical libraries that make life better. The fact that Flow has a repository of these definitions is good. How frequently they’re maintained was a different question..

Types can be imported

This is something I hadn’t thought about but is obvious in hindsight. If I declare a custom type in one file I want to be able to use it in another file. With Flow you can do this.

The syntax is succinct and obvious

I never really felt that the syntax got too much in my way nor did I find it confusing. The decision to declare types inline is a decision that both Flow and TypeScript share, and it does add a fair amount of verbosity where there was a lack of verbosity before, but it was quick to get used to.

Good Type Inference

Flow’s compiler was surprisingly good at catching inferred type errors. Type inference is great for keeping verbosity down while maintaining type safety. Nice work Flow team!

The Meh

I’m going to breeze through these due to the unexciting nature of a “meh” finding.

  • You are forced to parenthesized single argument arrow functions that you wish to annotate.
  • I had to ignore certain node_modules directories in my .flowconfig file. I wasn’t much interested in why, just that I wanted to move on and try out Flow.
  • Functions that have several arguments became much more verbose than before. The trade-off of having typed functions at the cost of verbosity made this point a wash for me.

The Bad

I imagine this is the section that some of you scrolled straight to so let me preface this section with some qualifying remarks. The following points are “bad” only as a matter of my opinion and what’s important to me. Please, do not read this section and use it as justification to go try something else. Define what’s important to you, try Flow for yourself, and make up your own mind on whether Flow is right for your situation.

Jarring error messages

Allow me to admit some bias here and say that I’m coming from Elm, a language with a compiler that has super helpful error messages. This is not the same experience that I get with Flow’s compiler. Flow’s error messages were hit and miss at best, and jarring at worst.

A type error reported by Flow

If I look at this error long enough I can pick up what the problem is. I’m assigning a string to taxQuoteAmount even though it’s defined as a number. But the error message is a multi-line message with code mixed with error messages, and it has me looking at four different line numbers. The part where it says “This type is incompatible with” without finishing the sentence was especially confusing as a Flow beginner.

After seeing this kind of error enough times my eyes developed a sort of muscle memory and I automatically ignored most of the error to get to the parts that told me what I need to know. But I’m not a fan of that developer experience.

In the end, I think it’d be less jarring and more helpful if the error simply pointed me to line 233 and told me “You’re assigning a string to taxQuoteAmount but taxQuoteAmount is defined as a number”.

Relatively small community compared to TypeScript

This fact normally wouldn’t bug me too much. Once a community gets to a certain size I’m not all that worried about who has more retweets, which Github repos have more stars, etc. It wouldn’t be problem except for the part where a stronger community makes a difference, and that difference for me was in the Library Definition repository.

Perhaps it was just the libraries I was looking for, or perhaps it was the particular versions of libraries that I was looking for, but if I was going to use Flow on my project I’d have to downgrade one of our “critical” libraries and remove/replace another. This makes using Flow harder to justify if I gain types but at the cost of losing the benefits of another library.

It may turn out that the public TypeScript type definition repository has the same problems. At which point this “bad” bullet point is no longer unique to Flow.

Type Black Holes

I can’t remember where I saw that phrase, so apologies to whoever came up with it, but it’s an apt phrase that can undermine the type safety of your application.

To the best of my understanding, when Flow comes across a library that it doesn’t have definitions for it will generate a library definition for that library but with very loose types, such as using the any type.

If you’re either passing typed data to that library, or expecting typed data in return, those types can get lost in a “black hole” and replaced with an any type. All of the sudden, and perhaps without you realizing, your code is compiling fine because any means literally anything and code isn’t type checking correctly.

Exact Object Types aren’t the default

Following Flow’s documentation my first go-to for creating a custom type was using a type alias. As it turns out, a type alias defines the minimum properties on an object. The minimum, not the complete set of properties.

I found out while doing a simple array mapping that I could produce data that was a superset of my typed alias. I could add extra properties to a record and still pass the type checker. An untyped superset of a type alias will ultimately do more harm than good.

The thing I was really looking for is called an Exact Object Type. It defines a custom type exactly and with negligible extra syntax. However, questions were popping up in my head. How do I enforce my team to use Exact Object Types? How do I know if library definition files are using Exact Object Types?

I don’t know if these questions have answers, so please point me at them if they do.

Can’t use the spread operator with Exact Object Types

This point is more relevant because we’re using Redux and we need a succinct way to treat data as if it was immutable. Our immutability solution wasn’t working with its library definition file, so Plan B was to use the spread operator but it turns out it the spread operator won’t work with exact types.

This would leave us with some unfortunate options:

  1. Find a different immutability library that has a working library definition
  2. Write our own library definition file for the immutability library we’re using
  3. Use the spread operator with type aliases instead of exact object types and write unit tests that effectively type check the code to make sure additional properties aren’t accidentally added.

Summarizing the bad

It’s worth reminding readers the title of this post. It’s not titled “Why you should avoid Flow”. It’s really about my optimism towards using Flow and the “bad” points discovered above left me less excited than I was before.

It’s also worth noting that I haven’t looked this deep into TypeScript yet. TypeScript and Flow may share some of these concerns. I suspect I will find out soon enough.

The Good Again

It’s easy to criticize something that I didn’t write and it’s easy to forget that humans created and actively maintain Flow with the intent of making our Javascript development lives better. It has to be challenging to create a typed solution in the Javascript ecosystem we find ourselves in.

I really appreciate the hard work that the Flow team is doing. Javascript needs a type solution and I think Flow has some foundational decisions that were correctly made. Hopefully this single experience can be used as input to move Flow further in a beneficial direction.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.