Patrick Desjardins Blog
Patrick Desjardins picture from a conference

TypeScript vs Flow (Part 2 of 3) - High Level Coding

Posted on: 2017-08-23

This is the second article of three about differences between TypeScript and Flow. You can read the first part about high level of TypeScript vs Flow in this article. The investigation I conducted was done on August 9 and August 10, 2017. It's a domain where it evolves a lot and my conclusion could change in few months or years. The first article goal was to bring fundamentals around what are TypeScript and Flow.

This article will focus on high-level coding feature that differentiates TypeScript and Flow. The get differences, I had to sample a subset of all the feature and the reason is that both support a lot of them and I have limited time. How I decided these features was to go with Flow first by using their documentation. My plan was to convert one TypeScript project already written to Flow and while doing so, I added features that were used which resulted to the features comparison matrix of this article. The result is of the 40 features tested is:

  • 11 features lean on TypeScript
  • 4 features lean on Flow
  • 18 features that are neutrally equal
  • 1 feature that is subjective who is better

Let's spread the 40 features in a table to illustrate these details and then talk about them.

FeatureTypeScriptFlowWinner?
InterfaceYesYesBoth
Type (Alias)YesYesBoth
SubtypeYesYesBoth
ImplementationYesYesBoth
Encapsulation (private, protected, public)YesNoTypeScript
EnumYesNoTypeScript
$key (utility type)NoYesFlow
$diff (utility type)NoYesFlow
* (utility type)NoYesFlow
Type CastingYesYesBoth
DefinitionFile or Librairie DefinitionYesYesBoth
Non standard JSYesYesBoth
Get definition fileNPMcliTypeScript
ReadonlyYesNo (see covariance)TypeScript
React props/statesYesYesBoth
maybe/optional parameterYesYes (separate null/undefined)Both
Optional propertiesYes (same syntax)Yes (diff syntax)Both
Infer typeYesYesBoth
ContravariantYesYesBoth
CovarianceNo (see readonly)YesFlow
AbstractYesNoTypeScript
DecoratorYesNoTypeScript
Quick initialization (by ctor params)YesNoTypeScript
StaticYesNot all scenariosTypeScript
SealedYes by defaultYes by defaultBoth
UnsealedMust castObject defined by Both
TupleYesYesBoth
SoundnessNot completelyYes???
Type of typingStructural typingNominally typingTS is following JS
Generic ClassYesYesBoth
Generic InterfaceYesYesBoth
Generic FunctionYesYesBoth
Generic TypeYesYesBoth

That's a lot of information and at the same time, just a sample of what Flow and TypeScript can offer. I'll try to cover the difference without focusing on the resemblances and more about what makes them apart.

Encapsulation

Encapsulation or "modifier" is the first difference. I'll talk about the lack of private, public, protected keyword all at once. This level of protection doesn't exist in JavaScript, but TypeScript allows it if interested, otherwise, it's all open (public). TypeScript enforces it at the syntax level put the transpiled JavaScript doesn't have any trace of the encapsulation notion. For example:

class MyClass {
    private privateMember: string;
    public publicMember: string;
    protected protectedMember: string;
    constructor(message: string) {
        this.privateMember = message;
        this.publicMember = "public";
        this.protectedMember = "protected";
    }
}

Transpile to:

var MyClass = (function () {
    function MyClass(message) {
        this.privateMember = message;
        this.publicMember = "public";
        this.protectedMember = "protected";
    }
    return MyClass;
}());

The encapsulation is not propagated in JavaScript, but could have been there with some closure. In all circumstances, Flow decided to not enforce the encapsulation at all. The repercussion is interesting. First, the code becomes harder to maintain because it allows people to access members directly. A common scenario is to inject by constructor some specific class that represent services. You want to control the integrity when the class is instantiated, but if the injected objects are public, they could be modified by anyone in the lifetime of the object. There is much other justification why having a stronger encapsulation is good which will be the subject of future articles.

Enum

enum is a convenient way to organize potential value for a type. For example, with TypeScript you can write an enum that will hold a specific group of language the following way:

enum Language {
    English,
    French
}

This syntax allows to assign easily with a good Intellisense support, but also enforce passing only the value from the enum by parameter.

var myLanguage = Language.English;
functTakeOnlyLanguage(myLanguage);
function functTakeOnlyLanguage(param: Language){
    // ...
}

The notion of Enum doesn't exist in Flow [2]. They have a workaround which more verbose [3] than the TypeScript keyword. Here is the same representation with Flow.

export const LANGUAGES_VALUE = {
    French: "French",
    English: "English"
};
export type Language = $Values<typeof LANGUAGES_VALUE>;

var myLanguage = LANGUAGES_VALUE.English;
functTakeOnlyLanguage(myLanguage);
function functTakeOnlyLanguage(param: Language){
    // ...
}

Using this type and constant with Flow require to have a special import when using.


import { LANGUAGES_VALUE } from "../general/Language";
import type { Language } from "../general/Language";

The Flow version requires a little more work since you need to define the const and then use a "magic utility type" to create a type, and after you need two imports which one is non-standard. The use of "magic" seems pejorative, but it's how it is called. Also, it might be the configuration issue between my VsCode and Flow during my investigation, but the type of Language was "any", hence the Intellisense wasn't great. Overall, TypeScript have an edge here on a simple thing that makes developer life just a little easier.

Utility Type

Flow is having something TypeScript does not literally have called utility type [3] or magic utility type. We already saw the potential with Enum with the $keys one. TypeScript lets you do something similar with keyof.

Flow has also $Diff<a,b> which returns a type that is the difference between two types. There is also few others which are not all implemented. </a,b>

So, at first glance, it's a pro and I marked it as well. However, the more I was thinking about it and the more I was wondering why Flow has these utility method. It should belong somewhere else than inside a type checker tool. These utility methods would dwell well in a Babel plugin, or in a library like lodash, or directly in TypeScript. I do not have the full context, but it seems that it was added to fulfill a need with React which is a close team Flow is working with.

Getting Definition Files

Getting definition files is different between TypeScript and Flow. The reason is that Flow doesn't use the same definition file than TypeScript. There is no way to translate one to the other, and both persist their definition files in an independent repository. This creates fragmentation in the community that needs to create two kinds of definition file. As mentioned in the first part, TypeScript has above three thousand definition files while Flow has reached few week ago three hundred. Besides the number of libraries, the major difference is how to get the definition files. TypeScript has a long history of ways to do it. At the time I am writing this article, we are at the third iteration which seem to be stable and better in many ways. It's now using NPM, a tool that every JavaScript developers have familiarity since it is the most common way to get library. It makes sense to have the definition file by the same way. Using NPM allows using the same syntax and configuration file (package.json). This is convenient to store data, but also convenient to get the definition file from a repository that you do not own. By installing with "npm install" you are getting all the libraries as well as all the definition files.

npm install --save-dev @types/redux

With Flow, getting library definition is not hard, but it's not a paved way what we are used to ride on. It requires using a cli (command line)[4]. This cli is available from npm (npm install -g flow-typed) and from there, you can use flow-typed to get a library that will be installed into a flow-typed folder at the root of your project. One requirement is that you must specify a version.

flow-typed install redux@2.3.x

If you get a project and needs to get the library, you will need to use npm install but also flow-typed install. Since we need to handle the second tool, Flow doesn't gain a point in that comparison. TypeScript matured into using NPM and that choice simplify the access to third party definition file.

Readonly

TypeScript has the keyword readonly. Flow does not. However, Flow has something similar which is the + sign for covariance. The goal is to do like in C# or another language which is to set a value directly in the class where we the field is declared or when in the constructor when the class is instantiated. At the end, the value cannot change. This allows having values that are set dynamically and only once.

Flow doesn't have the keyword read only. It has the concept of covariance that TypeScript doesn't have. To mimic this behavior, in Flow it requires having a + sign in front of the variable. The difference is small for a field but subtle. For example, it works well if you know the value and assigns it directly to the field of the class. However, if you want to assign it in the constructor, it won't work in Flow. Let's see some code:

class Person {
  +name: string;
  constructor(name: string) {
    this.name = name; // This doesn't work in Flow
  }
}

But the equivalence work in TypeScript:

class Person {
  readonly name: string;
  constructor(name: string) {
    this.name = name
  }
}

The case of read only is simpler with TypeScript and work like many other languages, hence seem to be slightly favorable for TypeScript.

Optional Parameter and Optional Field

Both have a different way to achieve optional value to be provided for parameter of a function or to have a field defined (in an interface for example). TypeScript use the question mark in both cases.

// Optional Field
interface IInterface{ propertyName?: string } //Allow null amp;amp;amp;amp; undefined

// Optional Parameter
function myFunc( param?: string ) //Allow null amp;amp;amp;amp; undefined

This is different from Flow which will not be that loose. With Flow, you can pass nothing (which is undefined) but cannot explicitly pass null for something optional.

// Optional Field
interface IInterface{ propertyName?: string } //Allow undefined

// Optional Parameter
function myFunc( param?: string ) //Allow undefined

If you want to pass null, you need to use the concept of "maybe" which is unique to Flow. The syntax is also with the question mark, but this time before the type.


// Optional Parameter
function myFunc( param: ?string ) //Allow null amp;amp;amp;amp; undefined

This goes in the sense that Flow is more "sound" than TypeScript. Flow has more control about what you want to pass. TypeScript is following JavaScript and is less strict in that matter. At the end, both sides can be pros or cons. TypeScript shines by having a single place to put the question mark and it's always the same logic which is that it allows value, null and undefined. On the other hand, Flow has more option by being able to allow value an undefined as well as null if using the maybe type. However, the syntax is very similar to the other type of optional which can cause confusion. At the end, they have different pros and cons and I consider no winner.

Abstract

Abstract is available in TypeScript; it's not in Flow. Abstract is an object oriented concept, and it wasn't available 3 years and half ago when I started using TypeScript and it was a tool that I missed. Since about 2 years, it's available and I used it few times. I do not want to explain why it can be useful or not in this comparison article, but it's a concept that some kind of developers like while other doesn't. TypeScript doesn't force you to use it, and give you the ability to if you want. By giving the developer the choice instead of not providing it, TypeScript wins.

Decorator

TypeScript requires having a configuration flag to true in the tsconfig.json. Flow requires having a babel plugin. It needs a little more work, like getting the right package, but nothing significant. I started using the babel decorator, but was getting a warning message that conducted me to using the decorator-legacy.

From there, Flow was giving an error saying that this feature was experimental. Flow, at this time, doesn’t support decorator [5]. I ended up by having to ignore the EcmaScript proposal in the .flowconfig file. TypeScript won. However, if you are not using Angular2+ or MobX than you might do not mind. However, more and more libraries and frameworks use decorator which is nice to have some type checking as well.

Quick Initialization

I'll be brief. Since Flow doesn't have encapsulation than it's obvious that quick initialization doesn't exist in Flow. This feature allows to not declare inside a class the field and avoid having line of code to set the constructor's argument to the field manually. This is handy and a recent addition to TypeScript. It's clean up the code by still having a great encapsulation. TypeScript wins by default.

Statics

On this one, I wasn't very thorough. I had code that was defining readonly static field in TypeScript and couldn't have them static (without readonly) in Flow. So, I had to choose about having the value defined once or the field to be static. TypeScript acted more like I was used to see in Java or C#. Because of that, I'm giving a weak win to TypeScript.

Type of Typing

TypeScript is using a structural typing [6]. Flow is following nominally typing [7]. It means that for TypeScript, if you have two interfaces or classes with the same members but with a different name, hence are a different type, they are assignable. This is to mimic how JavaScript works. However, Flow is nominally typed which means that every class or interface defined is unique and cannot be assigned to another one even if they have the same members. I could elaborate more, but I suggest you read the TypeScript documentation about type compatibility. So who is winning? It’s a hard one. Flow wins if we want something more strict. TypeScript wins if we want not to invent a new language on top of JavaScript. To be noted is that, even if JavaScript doesn’t have keywords for encapsulation and abstraction, the language allow it with closures and other JavaScript tricks. So at the end, TypeScript seems to be the winner since it doesn’t try to change the nature of JavaScript, which Flow is doing…and maybe for the best? I guess that is debatable.

Conclusion

This part is getting long. I’ll end it here. In the first part, we saw some fundamental differences between TypeScript and Flow. In this second part, we saw that the two type checkers are pretty close in features. Even if I write about the differences, there were so many similarities that I got impressed and surprised. In the last and third part, I’ll take the time to show in terms of code the similarities and differences. Before closing this part two, if you are still hesitating on which one to use, you won’t find more answers in the third part. The next article will confirm the idea that both of them are pretty similar. I suggest that you read again the first part and this one to make your decision and read the third one only for your curiosity.

Parts of the serie:

References