TypeScript vs Flow (Part 3 of 3) - Syntax Difference<!-- --> | <!-- -->Patrick Desjardins Blog
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

TypeScript vs Flow (Part 3 of 3) - Syntax Difference

Posted on: August 30, 2017

This is part three of three of TypeScript vs Flow. We previously saw the fundamental differences between TypeScript and Flow, then we saw some high-level differences in terms of features. Now, we will see at a much lower level the differences at the syntax.


TypeScript and Flow are exactly the same.



Again, no difference.



And again, no difference.



TypeScript is shorted:

1// Language file:
2export enum Language {
3 English,
4 French
6// Use of Language file:
7import { Language } from "../general/Language";
8export class LocaleManager {
9 public static GetLocale(language: Language): void {
10 switch (language) {
11 // ...
12 }
13 }

Flow requires the value in const, and special import. It's more verbose.

1// Language file:
2// @flow
3export const LANGUAGES_VALUE = {
4 FRENCH: "French",
5 ENGLISH: "English"
1export type Language = $Values<typeof LANGUAGES_VALUE>;
2// Use of Language file:
3import type { Language } from "../general/Language";
4import { LANGUAGES_VALUE } from "../general/Language";
6export class LocaleManager {
7 static GetLocale(language: Language): void {
8 switch (language) {
9 // ...
10 }
11 }

Maybe Argument

Watch out the question mark in the few next examples. Flow distinguishes null and undefined between optional and maybe.

3{ propertyName?: string } //Allow null & undefined


1{ propertyName: ?string } //Allow null & undefined

Optional Property


1{ propertyName?: string } //Allow null & undefined


1{ propertyName?: string } //Allow undefined

Optional Argument


1( propertyName?: string ) //Allow null & undefined


1( propertyName?: string ) //Allow undefined

Literal value as type

In both cases, same syntax. For example, the code below only accepts the value of two. The difference is that TypeScript allows us to pass null or undefined. Flow doesn’t. To have Flow accept null you need to use the maybe concept which is to put a question mark right before the two.

1function funct(value: 2) {}
2// Flow to accept 2 and null and undefined:
3function funct(value: ?2) { }
4// Flow to accept 2 and undefined by not null:
5function funct(value?: 2) { }

Union type

Union type are supported by both and have the same syntax. Indeed, TypeScript allows to pass null and undefined while Flow doesn't.

1function funct(arg1: "a" | "b" | "c") { }
2// Flow to have also null and undefined:
3function funct(arg1?: "a" | "b" | "c" | null) { }

Union primitive type

Union with primitive work the same. TypeScript is consistent by letting null and undefined as well.

1function funct(arg1: number | string) { }
2// Flow to accept null and undefined:
3function funct(arg1?: number | ?string) { }
4function funct(arg1?: number | string | null) { }

Mixed keyword

Mixed doesn’t exist with TypeScript, so here is how it’s written in Flow. In TypeScript, you would use any or an empty definition with the curly brace: {}. The difference is that with any or curly brackets you can redefine the variable with any type once initialized. With Flow and the use of mixed, it’s any type until it’s defined. Once defined, the type is set for the life of the variable. Nice addition to the language, it’s a “dynamic any”.

1function funct(arg1: mixed) { }

Any type

Same syntax.

1function funct(arg1: any) { }

Define return type

Returning value is with the semi-colon followed by the type. If nothing, return void.

1function funct(): string { }

Function type

The signature of a method is exactly the same. It uses the JavaScript keyword “function” with parentheses that contain the name followed by the type. After the semicolon, the type is returned.

1function funct(p1: string, p2?: boolean): void { }

Function w/o type

Function returns void by default in both.

1function funct(p1, p2) { }

Function fat arrow

Fat arrow is a copy and paste between the two.

1let funct = (p1, p2) => { }

Function with callback

Callback arguments are written the same way in TypeScript or Flow.

1function funct(cb: (e: string | null, v: string | number) => void) { }

Funct with rest

Both support the long and short version.

1function funct(...args: Array<number>) {}
2function funct2(...args: number[]) {}

Type Alias

Aliases are written the same way in both case.

1var obj1: { foo: boolean } = { foo: true };
2// or
3type MyType = { value: boolean };
4var obj2: MyType;

Type Alias with Generic

Generic doesn't change anything. TypeScript and Flow stays the same.

1type MyObject<A, B, C> = {
2 property: A,
3 method(val: B): C,
5var x: MyObject<number, string, boolean> = { property: 1, method: (string) => { return


Defining your own type is the same in Flow or TypeScript.

1type TypeLetters = "A" | "B" | "C";

Object as map

The first part is the same. You can use a string between square bracket to access in read or write a member of an object.

1var o: { foo?:number } = {};
2o["foo"] = 0;

However, TypeScript won’t allow dynamic adding of a member. In TypeScript, once a type is set to a variable, this one cannot change. The exception is the "any" type.

1var o: { foo?:number } = {};
2o["foo"] = 0;
3o[“bar”] = 2; //This will work in Flow, but not in TypeScript

This is interesting since Flow is looser in this regard, and TypeScript is looser in term of the null/undefined. The justification of TypeScript to be strict is its structural typing.


Pretty similar except the case of allowing Null. Here are all the similar scenarios.

1var x = [];
2var y = [1, 2, 3];
3var z = new Array();
4var w: boolean[] = [true, false, true];
5var v: Array<boolean> = [true, false, true];

Here is how TypeScript does with null (nothing to do) and how to add the possibility of null in Flow.

1var s: number[] = [1, 2, null];
2var s: (?number)[] = [1, 2, null];


Same syntax for tuple.

1var tuple: [number, boolean, string] = [1, true, "three"];

Key interpolation

1var obj1: { foo: boolean } = { foo: true };
2obj1["foo"] = false;

Opaque Type

With Flow you can define a type detail to be only available to a module. Here is an example. It can be used by an external module by importing it. However, the other file that imports won’t be able to assign it as Number, only as “MyType”. This feature can be interesting for people to enforce the use of their type. This is not available for TypeScript.

1opaque type MyType = number;

Interface and Implementation

Implementing an interface is the same in Flow or TypeScript.

1interface Serializable {
2 serialize(): string;
4class Foo implements Serializable {
5 serialize() { return '[Foo]'; }

Interface as map

Both share the same syntax.

1interface MyInterface {
2 [key: string]: number;
4var x: MyInterface;
5x.key = 3;

Interface and object

Both are the same for the first part.

1interface MyInterface{
2 foo: string,
3 bar: number
5var x: MyInterface = {
6 foo : "foo",
7 bar : 1

However, TypeScript is more permissive since you do not have to define both members. You can just define the type, without defining any value at all, and later assign the value. By default, the value will be undefined.

1var x: MyInterface;
2x.foo = "foo";
3x.bar = 1;

Covariance property (readonly)

The syntax is a little bit different. TypeScript is using a longer approach, similar to C# with the keyword "readonly". Flow is using the plus symbol.

1interface MyInterface{
2 readonly foo: string
4var x: MyInterface = { foo: "bar" };
5x.foo = "123"; // Blocked!


1interface MyInterface{
2 +foo: string
4var x: MyInterface = { foo: "bar" };
5x.foo = "123"; // Blocked!


TypeScript needs to have the type to be defined to all possible value. In the following example, the field is of type number. However, in the code, it can be a number or a string. TypeScript blocks the code to compile since there is a possibility of being a string, so it says that it must be a number or a string.

1interface Contravariant { writeOnly: number; }
2var numberOrString = Math.random() > 0.5 ? 42 : 'forty-two';
3var value2: Contravariant = { writeOnly: numberOrString }; // Doesn't work. Need to change the type number to be : number | string

With Flow, with the same syntax, you get the same result. However, with the sign minus, you can make the code to be contravariant.

1interface Contravariant { -writeOnly: number; }
2var numberOrString = Math.random() > 0.5 ? 42 : 'forty-two';
3var value2: Contravariant = { writeOnly: numberOrString }; // Works!

Again, this is a small syntax difference. At the end, isn't just simpler to just mark the type to "string | number" in both case?

Generic Class

Exact same syntax.

1class Item<T> {
2 prop: T;
3 constructor(param: T) {
4 this.prop = param;
5 }
7let item1: Item<number> = new Item(42);

Generic Interface

As we saw earlier, TypeScript allows to declare a type and not instantiated completely or completely. Flow must declare and instantiate.

1interface MyInterface<A, B, C> {
2 foo: A,
3 bar: B,
4 baz: C,

// Flow and TypeScript:

1var x: MyInterface<string, number, boolean> = {foo:"1", bar:4, baz:true};

// Only TypeScript:

1var x: MyInterface<string, number, boolean>; // Notice that we only declare.
2x.foo = "123";

Generic Type

Both allows to have generic type in the same syntax.

1type Item<T> = {
2 prop: T,

Generic Function

Generic function share the same syntax between Flow and TypeScript.

1function funct1<T>(p1: T, p2 = "Smith") { }

Default parameter value

Default parameter use the equal sign followed by the value in both cases.

1function funct1(p1: string, p2 = "Smith") { }

Generic Type with default type and value

TypeScript uses the keyword extends to have a generic type from a particular type. Default value is the same.

1Type Item<T extends number = 1> = {
2 prop: T,

Flow uses the semicolon to enforce the generic type and equal sign for default.

1type Item<T: number = 1> = {
2 prop: T,


Cast is different. Require having parentheses and a semi colon in Flow. TypeScript uses "as" like in C#. Flow is a little bit more verbose and can become harder to see in a situation within the inner function with many parentheses and curly braces.

1var x: number = otherVariable as number;
2var x: number = (othervariable: number);

While I was investigating Flow, I was getting more and more surprised by the similitude with TypeScript. In this third article, we can see that the syntax is so common with TypeScript that it’s hard to be very polarized about which one is really the best. There are few fundamental differences that we saw in terms of concept, but even there it’s not very deeply different. Indeed, Flow is checking more scenarios, and TypeScript is letting some scenarios be more “JavaScript-ish”, but still, in other scenarios allows a strong rigidity and better encapsulation.

Every team is different. Every person has a different past. Flow and TypeScript should cover the need of having your code more strongly typed, less prone to error, and easier to understand by everyone who reads it. The choice of Flow or TypeScript is a matter of little details. At the end of my investigation, I was all open to going with Flow for our new project at Netflix. However, the last meeting tipped the decision to go with TypeScript. In our case, the number of supported third-party libraries was something that changed our mind.

Parts of the serie: