Flow: A Static Type Checker for JavaScript

Lou Mauget Development Technologies, JavaScript, React Leave a Comment

Attention: The following article was published over 5 years ago, and the information provided may be aged or outdated. Please keep that in mind as you read the post.

In this post, we’ll discuss type concepts, compare static and dynamic types, and show an unobtrusive type inference package provided by Flow.org.

Facebook developed and maintains Flow. The package provides static typing to normally late-bound JavaScript code, including React code. It provides this analysis to a JavaScript application, even if it is an existing application.  Flow operates by carrying out a static abstract syntax tree (AST) analysis of type flows at build time.

Usage

Let’s kick the tires. Add a small annotation in a comment at the top of a source file:

// flow

Now paste the file contents into Flow’s type inference analysis at https://flow.org/try/. Flow determines static types by inferring them via static flow analysis. This shifts type checking away from execution time. In actual use, we’d configure Flow as part of a build process, as directed at https://flow.org/en/docs/getting-started/.

The goal? To find type-conflict errors at build time instead of in production. If ambiguities surface at build-time, we would either apply Flow annotation to hotspot variables for analysis guidance or address any actual type error flagged in a Flow message.

We can insert annotations in special JavaScript comments, or as additions that a post-processing build step would strip. The latter approach doesn’t obscure the logic after a post-process (e.g. Babel), strips the annotations after analysis. We’ll touch on type annotations after we look at types and at when JavaScript applies them.

What ARE Data Types?

A 25-cent paraphrase: “A data type is the set of operations and methods valid for a given data item.” See the references for the five dollar description.

Dynamic vs. Static Typing

  • Dynamic typing: an item’s data type IS NOT settled until runtime. It is late-bound.
  • Static typing: the item’s type IS known at build-time. It is early-bound, either by explicit setting or by type inference. The latter notion is the heart of Flow.

Dynamic Typing

This is soft-typing. JavaScript string, array, and int are types. Each represents qualities and behaviors of an associated data item. For example, a string and an array each have a slice method property. An int does not. A string and an int each understand a differing + operator.

Here’s an example JavaScript file that exercises a seven-permutation subset of + or slice on pairs of string, int, or array involving a variable and literals.

let c = 123;
console.log(c + "string");
c = 'string';
console.log(c + 123);
c = [123];
console.log(c + 123);
c = 123;
console.log(123 + 123);
c = 'string';
console.log(c.slice(0,1));
c = [123];
console.log(c.slice(0,1));
c = 123;
console.log(c.slice(0,1));

The execution sysout stream:

The runtime handled six of the operations by dynamically casting data types. That c variable had multiple personality disorder. That was Duck Typing in action. This late-bound typing can make programming satisfying and fun for developers – until it has a miss at runtime. Think “production.”

Our snippet’s execution tripped over the seventh demo expression despite its legal syntax. It tried to slice an int. As you know, those are impervious to knives.

Why didn’t the parser know this? Because, being of a late-bound persuasion, it didn’t notice that the type of “c” became an “int”. In other words, it didn’t track the flow of a literal int into the variable. Not to worry: runtime caught the wrong type for the operation, belching a red error message. Luckily, the code was not transferring a paycheck into our bank account.

The parser would have known the type of a literal if one were substituted for the c. Literals are easy to check, being frozen in type and value. Variables? They have a habit of…varying. Alas, they’re crucial to real logic, so we’re forced to deal them…and their dynamic types.

How would a developer defend against our error? Maybe by a logic change or by a typeof check? They could ideally scan the application for such opportunities while being careful not to introduce a new bug. We could argue that “defense” is a distraction from the task-at-hand. Here, the luster of Duck Typing seems to fade.

Static Typing

This is hard-typing. If the language-at-hand uses pure static typing, its parser knows applicable valid operations and methods. Parsing errors cause the logging of invalid situations. Such early binding historically came at the expense of a developer requirement to set types on each variable or constant.

It crushed some of the exhilaration provided by Duck Typed languages. On the other hand, the developer gained confidence that her logic would not fail because of a type of conflict.

Advantage of Static Typing

Let’s cut over to Java for a minute. There, any pre-1.6 compiler requires explicit static types. This reduces runtime surprises by ensuring correct type usage at compile-time.

Here’s an example using types tagged with angle bracket generics. The coder simply wants to make a list of strings.

	List<String> list = new ArrayList<String>(); // Java 1.5

That seems wordy compared to a JavaScript rendition:

	const list = []  // es6 JavaScript

Simple. A const cannot be reassigned, so the type remains an array forever. That defends against a type-change at run time.

Yes, JavaScript list can accept pushes of a mixture of types. What if client logic assumes the array contains only strings? The type-protected Java counterpart enforces this assumption. Extra code to constrain the JavaScript snippet’s element types stirs the central thought. The added code may tend to obscure the central task.

Static typing could constrain those types to string, maintaining readability while minimizing bugs due to potential defensive errors.

Downside of Static Typing

That Java statement is noisy compared to the JavaScript counterpart, right? The developer and maintainer must deal with distracting chaff. Yes, a 1.6 JVM allows a simple <> for the brackets, but the List and ArrayList still seem to be redundant noise.

We had to tell the compiler that the list is a List. An ArrayList IS a List, isn’t it? Verbosity multiplied across the application means less readability. A blur of code inhibits brilliant reasoning while exacerbating future misunderstandings.

Type Inference

How about a cool way to enjoy the confidence upside to static typing while minimizing developer-supplied type-noise? Say “type inference!” Thank you, academia and functional programming! How do you pronounce Haskell?

Don’t worry. We’re not advocating migrating JavaScript to Haskell. We can now apply type inference to JavaScript object-oriented, functional, and JSX code. This is static typing without specifying all types explicitly.

The goal: catch type errors at build-time without requiring much manual specification of types.

Hindley–Milner Type System

This type of inference paradigm originated in academia. It works well, but the implementation is complex. No, we’re saying that it’s complex! Haskell and Scala implemented it. Others followed.

Facebook implemented it as an annotation-driven filter. It analyses JavaScript, reports any data type conflicts, but doesn’t need to permanently modify JavaScript source code. That is our friend, Flow.

Try Flow Online

Let’s enter that earlier seven-part snippet into the “Try” view of https://flow.org/ . The result:

Flow flagged line 16, the line that parsed error-free, but the line blew up in execution. Flow flagged line 8, as well, but that line executed okay. Was its result what we intended? Let us use a cast on those two lines to direct the parser.

Here is the Flow “Try” box for that code after placing the casts at lines 8 and 16. Success.

Following is the console after executing that version of the snippet:

Exception cured. At this point, we increased our confidence in this monument to destiny. Again, the beauty of the Flow approach is that we can open legacy code for analysis during the build, but without changing logic or language.  Certainly, at some point, Flow may flag a type ambiguity. There, we’d need to provide a hint via a type annotation or make a logic fix.

Flow Configuration and Usage

Flow Installation is covered at https://flow.org/en/docs/install/. The page at https://flow.org/en/docs/usage/ discusses usage. It’s vanilla npm, an auto-created config file,  and easy.

Flow Type Annotations

The complete list of type annotations is in the API document at https://flow.org/en/docs/types/. We usually don’t apply a type annotation until Flow processing shows an ambiguity. The advantage of type inference is static typing, but with vastly reduced explicit typing.

Exception: a data container object may need proactive annotating. The remainder of the flow analysis may complete with less fuss after central data containers receive annotations. They are usually simple holders of members that have (wait for it)…types.

Real Example

We submitted an existing Node REST application to Flow. We received a huge set of type complaints in response. We added annotations to an ES6 Person container class, as shown below.

Notice that the annotations resemble TypeScript or Adobe Flex types. Each follows its variable declaration. We had to add an additional set of instance meta-variables as well. See them between the class and the constructor? Post-processing strips them after the flow analysis.

Here is the reverted Person class after flow processing:

Our PersonRepository client of class Person needed an up-front PersonObj annotation to describe the shape of its client Person class. Its role is as an interface signature to the data container. Notice the PersonObj reference in the angle-bracket annotation on the Map of persons?

Again, after flow reporting, the PersonRepository copy reverted to the original for input to the downstream build process:

This illustrates the unobtrusive nature of Flow’s type-inference. With Flow, JavaScript behaves as if it were statically typed.

We didn’t change existing words or logic, except for type conflict repairs found by Flow issues. We encountered several! The overall result? A REST service that we feel is resistant to type errors during execution.

What About TypeScript?

TypeScript is great!  Microsoft developed and currently maintains TypeScript, a strict superset of JavaScript. TypeScript adds classes, interfaces, namespaces, and generics to JavaScript.  It provides static typing through annotations and types of inference for optional static typing at compile time.

The TypeScript TSC compiler compiles the code into JavaScript. Microsoft coded the compiler in TypeScript. The compiler can emit a declaration file that provides signatures that operate somewhat like a C header file. Those are useful for producing packages consumable by third-party TypeScript code. TypeScript is integrated into popular IDEs and build tools, such as Gradle.

Why use Flow if there is TypeScript? The two reportedly have influenced one another. Word on the street is that the type-checking is more-or-less equivalent except for smaller evolving details, such as nullable values.

Flow is a type checker useful for imposing unobtrusive type checking on any JavaScript application. Typescript is a compiled language useful for creating a new application. Those are distinct use-cases that could dictate the choice.

Wrap

We discussed:

  • Data types
  • Dynamic typing vs. static typing
  • Pros and cons of each
  • Type inference as a quieter static typing mechanism
  • Online Flow checking
  • Flow configuration and build-time usage
  • A pair of examples
  • Typescript compared to Flow

Give Flow a try. Paste your snippet-of-choice into the try-page at https://flow.org/try/ . It takes less than a minute.

References

These are worth clicking:

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments