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:
- Flow.org: https://flow.org/en/
- Type Inference: https://en.wikipedia.org/wiki/Type_inference
- Type System: https://en.wikipedia.org/wiki/Type_system
- Data Type: https://en.wikipedia.org/wiki/Data_type
- Javascript and Duck Typing: https://medium.com/front-end-weekly/javascript-and-duck-typing-7d0f908e2238
- JavaScript type hierarchy: https://stackoverflow.com/questions/19891453/what-does-the-built-in-object-hierarchy-look-like-in-javascript
- TypeScript: https://www.typescriptlang.org/