Anyone who deals with UI development in a bloody enterprise has probably heard of “typed JavaScript”, meaning “Microsoft’s TypeScript”. But besides this solution, there is at least one more common JS typing system, and also from a major player in the IT world. This is a flow from Facebook. Because of my personal dislike for Microsoft, I used to always use flow. Objectively, this was explained by good integration with existing utilities and ease of transition.

Unfortunately, we must admit that in 2021 flow is already significantly inferior to TypeScript both in popularity and in support from various utilities (and libraries), and it’s time to switch to the de facto TypeScript standard. But under this, I would like to finally compare these technologies, say a couple of farewell words flow from Facebook.

Why do you need type safety in JavaScript?

JavaScript is a wonderful language. No not like this. The ecosystem built around JavaScript is great. For 2021, she really admires the fact that you can use the most modern features of the language, and then, by changing one setting of the build system, transpile the executable file in order to support its execution in older versions of browsers, including IE8, it will not be by night remember. You can “write in HTML” (I mean JSX), and then using the babel (or tsc) utility, replace all tags with correct JavaScript constructs like calling the React library (or any other, but more on that in another post).

Why is JavaScript good as a scripting language that runs in your browser?

  • JavaScript does not need to be “compiled”. You just add JavaScript constructs and the browser must understand them. This immediately gives a bunch of convenient and almost free things. For example, debugging directly in the browser, which is not the responsibility of the programmer (who must not forget, for example, to include a bunch of compiler debugging options and corresponding libraries), but the browser developer. You don’t need to wait 10-30 minutes (real-time for C / C ++) while your 10k line project is compiled to try to write something different. You just change the line, reload the browser page, and observe the new behavior of the code. And in the case of using, for example, webpack, the page will also be reloaded for you. Many browsers allow you to change the code right inside the page using their dev tools.
  • This is cross-platform code. In 2021, you can almost forget about the different behavior of different browsers. You write code for Chrome / Firefox, planning in advance, for example, from 5% (enterprise code) to 30% (UI / multimedia) of your time, so that later you can customize the result for different browsers.
  • In JavaScript, you almost don’t need to think about multithreading, synchronization, and other scary words. You don’t need to think about thread blocking – because you have one thread (not counting workers). As long as your code does not require 100% CPU (if you are writing a UI for a corporate application), then it is quite enough to know that the code is executed in a single thread, and asynchronous calls are successfully orchestrated using Promise / async / await / etc …
  • At the same time, I don’t even consider the question of why JavaScript is important. After all, with the help of JS, you can: validate forms, update the content of the page without reloading it entirely, add non-standard behavior effects, work with audio and video, and you can even write the entire client of your enterprise application in JavaScript.

As with almost any scripting (interpreted) language, in JavaScript, you can … write broken code. If the browser doesn’t reach this code, then there will be no error message, no warning, nothing at all. On the one hand, this is good. If you have a large, large website, then even a syntax error in the code of the button click handler should not result in the site not being loaded entirely by the user.

But, of course, this is bad. Because the very fact of having something not working somewhere on the site is bad. And it would be great, before the code gets to a working site, to check all-all scripts on the site and make sure that they at least compile. And ideally – and work. For this, a variety of sets of utilities are used (my favorite set is npm + webpack + babel / tsc + karma + jsdom + mocha + chai).

If we live in an ideal world, then all-all scripts on your site, even one-line ones, are covered with tests. But, unfortunately, the world is not ideal, and for all that part of the code that is not covered by tests, we can only rely on some kind of automated verification tools. Which can check:

  • That you are using the JavaScript syntax correctly. It is checked that the text you typed can be understood by the JavaScript interpreter, that you did not forget to close the open curly brace, that string tokens are correctly quoted, and so on and so forth. Almost all build / transpile / compression / obfuscation utilities perform this check.
  • That the semantics of the language are used correctly. You can try to check that the instructions that are written in the script you wrote can be correctly understood by the interpreter. For example, suppose you have the following code:

This code is correct from the point of view of the syntax of the language. But from the point of view of semantics, it is incorrect – an attempt to call a method on null will cause an error message during program execution.

In addition to semantic errors, there can be even more terrible errors: logical errors. When the program runs without errors, but the result is not at all what was expected. Classic with addition of strings and numbers:

Existing static code analysis tools (eslint, for example) can try to track down a significant number of potential errors that a programmer makes in his code. For example:

  • Forbidding an infinite for loop with an incorrect loop termination condition
  • Disallowing Async Functions as Arguments to a Promise Constructor
  • Preventing assignments in conditions
  • other

Note that all of these rules are essentially constraints that the linter places on the programmer. That is, the linter actually reduces the capabilities of the JavaScript language so that the programmer makes fewer potential mistakes. If you enable all-all rules, you will not be able to make assignments in conditions (although JavaScript initially allows this), use duplicate keys in object literals, and you will not even be able to call console.log ().

Adding variable types and type-aware call checking are additional limitations of the JavaScript language to reduce potential errors.

Trying to multiply a number by a string

An attempt to access a non-existent (not described in the type) property of an object

Attempting to call a function with a mismatched argument type

If we write this code without type checkers, then the code is successfully piped. No means of static code analysis, if they do not use (explicitly or implicitly) information about the types of objects, will not be able to find these errors.

That is, adding typing to JavaScript adds additional restrictions to the code that the programmer writes, but it allows you to find errors that would otherwise occur during script execution (that is, most likely in the user’s browser).

JavaScript typing capabilities

 FlowTypeScript
Ability to set the type of a variable, argument, or return type of a functiona : number = 5;
function foo( bar : string) : void {
/*...*/
}
Ability to describe your object type (interface)type MyType {
foo: string,
bar: number
}
Restricting Values for a Typetype Suit = "Diamonds" | "Clubs" | "Hearts" | "Spades";
Separate type-level extension for enumerations enum Direction { Up, Down, Left, Right }
“Adding” typestype MyType = TypeA & TypeB;
Additional “types” for complex cases$Keys<T>, $Values<T>, $ReadOnly<T>, $Exact<T>, $Diff<A, B>, $Rest<A, B>, $PropertyType<T, k>, $ElementType<T, K>, $NonMaybeType<T>, $ObjMap<T, F>, $ObjMapi<T, F>, $TupleMap<T, F>, $Call<F, T...>, Class<T>, $Shape<T>, $Exports<T>, $Supertype<T>, $Subtype<T>, Existential Type (*) Partial<T>, Required<T>, Readonly<T>, Record<K,T>, Pick<T, K>, Omit<T, K>, Exclude<T, U>, Extract<T, U>, NonNullable<T>, Parameters<T>, ConstructorParameters<T>, ReturnType<T>, InstanceType<T>, ThisParameterType<T>, OmitThisParameter<T>, ThisType<T>

Both engines for JavaScript-type support have roughly the same capabilities. However, if you come from strongly typed languages, even typed JavaScript has a very important difference from Java: all types essentially describe interfaces, that is, a list of properties (and their types and/or arguments). And if two interfaces describe the same (or compatible) properties, then they can be used instead of each other. That is, the following code is correct in typed JavaScript, but clearly incorrect in Java, or, say, C ++:

This code is correct from the point of view of typed JavaScript since the MyTypeB interface requires the foo property with the string type, while the MyTypeA variable does.

This code can be rewritten a little shorter, using the literal interface for the variable myVar.

The type of the variable myVar in this example is the literal interface { foo: string, bar: number }. It is still compatible with the expected interface of the arg argument to myFunction, so this code is error-free from the point of view of, for example, TypeScript.

This behavior significantly reduces the number of problems when working with different libraries, custom code, and even just calling functions. A typical example is when some library defines valid options, and we pass them as an options object:

Note that the OptionsType is not exported from the library (nor is it imported into custom code). But this does not prevent you from calling the function using the literal interface for the second argument of the function’s options, and for the typing system – to check this argument for type compatibility. Trying to do something like this in Java will cause clear confusion among the compiler.

How does this work from a browser perspective?

Neither Microsoft’s TypeScript nor Facebook’s flow is supported by browsers. As well as the newest JavaScript language extensions have not yet found support in some browsers. So how is this code, firstly, checked for correctness, and secondly, how is it executed by the browser?

The answer is transpiling. All “non-standard” JavaScript code goes through a set of utilities that turn the “non-standard” (unknown to browsers) code into a set of instructions that browsers understand. And for typing, the whole “transformation” is that all type refinements, all interface descriptions, all restrictions from the code are simply removed. For example, the code from the example above turns into …

that is

This conversion is usually done in one of the following ways.

  • To remove type information from flow, the babel plugin is used: @ babel/plugin-transform-flow-strip-types
  • You can use one of two solutions to work with TypeScript. Firstly one can use babel and the @ babel/plugin-transform-typescript plugin
  • Secondly, instead of babel, you can use Microsoft’s own transpiler called tsc. This utility is built into the application build process instead of babel.

Examples of project settings for flow and for TypeScript (using tsc).

FlowTypeScript
webpack.config.js
{
test: /\.js$/,
include: /src/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.(js|ts|tsx)$/,
exclude: /node_modules/,
include: /src/,
loader: 'ts-loader',
},
Transpiler settings
babel.config.jstsconfig.json
module.exports = function( api ) {
return {
presets: [
'@babel/preset-flow',
'@babel/preset-env',
'@babel/preset-react',
],
};
};
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": false,
"jsx": "react",
"lib": ["dom", "es5", "es6"],
"module": "es2020",
"moduleResolution": "node",
"noImplicitAny": false,
"outDir": "./dist/static",
"target": "es6"
},
"include": ["src/**/*.ts*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
.flowconfig
[ignore]
<PROJECT_ROOT>/dist/.*
<PROJECT_ROOT>/test/.*
[lints]
untyped-import=off
unclear-type=off
[options]

The difference between babel + strip and tsc approaches is small in terms of assembly. In the first case, babel is used, in the second, it will be tsc.

But there is a difference if a utility such as eslint is used. TypeScript for linting with eslint has its own set of plugins to help you find even more bugs. But they require that at the time of analysis by the linter it has information about the types of variables. To do this, the only tsc should be used as a code parser, not babel. But if tsc is used for the linter, then it will be wrong to use babel for building (the zoo of utilities used should be minimal!).

FlowTypeScript
.eslint.js
module.exports = { parser: 'babel-eslint', parserOptions: { /* ... */ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { /* ... */

Types for libraries

When a library is published to the npm repository, it is the JavaScript version that is published. It is assumed that the published code does not need to undergo additional transformations in order to use it in the project. That is, the code has already passed the necessary traspilation via babel or tsc. But then the information about the types in the code is already lost. What to do?

In flow, it is assumed that in addition to the “pure” JavaScript version, the library will contain files with the .js.flow extension, containing the source flow code with all the type definitions. Then, when analyzing the flow, it will be able to connect these files for type checking, and when building the project and its execution, they will be ignored – ordinary JS files will be used. You can add .flow files to the library by simple copying. However, this will significantly increase the size of the library in npm.

In TypeScript, it is not suggested to keep the source files side by side, but only a list of definitions. If there is a myModule.js file, then when parsing a project, TypeScript will look for the myModule.js.d.ts file nearby, in which it expects to see definitions (but not code!) Of all types, functions, and other things that are needed for type parsing. The tsc transpiler is able to create such files from the source TypeScript on its own (see the declaration option in the documentation).

Types for legacy libraries

For both flow and TypeScript, there is a way to add type declarations for those libraries that do not initially contain these descriptions. But it is done in different ways.

For flow, there is no “native” method supported by Facebook itself. But there is a flow-typed project that collects such definitions in its repository. In fact, a parallel way for npm to version such definitions, as well as a not very convenient “centralized” way of updating.

The TypeScript standard way of writing such definitions is to publish them in special npm packages with the “@types” prefix. In order to add a description of types for a library to your project, you just need to connect the corresponding @types-library, for example @types/react for React or @types/chai for chai.

Comparison of flow and TypeScript

An attempt to compare flow and TypeScript. Selected facts are collected from Nathan Sebhastian’s article “TypeScript VS Flow”, some are collected independently.

Native support across various frameworks. Native – no additional approach with a soldering iron and third-party libraries and plugins.

Various rulers

FlowTypeScript
Main contributorFacebookMicrosoft
Websiteflow.orgwww.typescriptlang.org
GitHubgithub.com/facebook/flowgithub.com/microsoft/TypeScript
GitHub Starts21.3k70.1k
GitHub Forks1.8k9.2k
GitHub Issues: open/closed2.4k / 4.1k4.9k / 25.0k
StackOverflow Active2289146 221
StackOverflow Frequent12311 451

Looking at these numbers, there is simply no moral right to recommend flow for use. But why was it actively used before? Because they’re used to be such a thing as flow-runtime.

flow-runtime

flow-runtime is a set of plugins for babel that allows you to embed flow types into runtime, use them to define variable types at runtime, and, most importantly for me, allow you to check the types of variables at runtime. That allowed at runtime during, for example, autotests or manual testing, to catch additional bugs in the application.

That is, right at runtime (in the debug assembly, of course), the application explicitly checked all types of variables, arguments, the results of calling third-party functions, and everything else for compliance with those types.

Unfortunately, for the new year 2021, the author of the repository added information that he is no longer engaged in the development of this project and, in general, switches to TypeScript. In fact, the last reason to stay on flow is gone. Well, welcome to TypeScript.