Patrick Desjardins Blog
Patrick Desjardins picture from a conference

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

Posted on: 2017-08-30

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.

Declaration

TypeScript and Flow are exactly the same.

Const 
let 
var

Primitive

Again, no difference.

Number 
Boolean 
String 
Null 
Undefined 
Void

Array

And again, no difference.

[] 
Array<T>

Enum

TypeScript is shorted:

// Language file:
export enum Language {
 English,
 French
}
// Use of Language file:
import { Language } from "../general/Language";
export class LocaleManager {
 public static GetLocale(language: Language): void {
 switch (language) {
 // ...
 }
 }
}

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

// Language file:
// @flow
export const LANGUAGES_VALUE = {
 FRENCH: "French",
 ENGLISH: "English"
};
export type Language = $Values<typeof LANGUAGES_VALUE>;
// Use of Language file:
import type { Language } from "../general/Language";
import { LANGUAGES_VALUE } from "../general/Language";

export class LocaleManager {
 static GetLocale(language: Language): void {
    switch (language) {
    // ...
    }
 }
}

Maybe Argument

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

TypeScript:

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

Flow:

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

Optional Property

TypeScript:

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

Flow:

{ propertyName?: string } //Allow undefined 

Optional Argument

TypeScript:

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

Flow:

( 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.

function funct(value: 2) {}
// Flow to accept 2 and null and undefined:
function funct(value: ?2) { }
// Flow to accept 2 and undefined by not null:
function 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.

function funct(arg1: "a" | "b" | "c") { }
// Flow to have also null and undefined:
function 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.


function funct(arg1: number | string) { }
// Flow to accept null and undefined:
function funct(arg1?: number | ?string) { }
function 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”.

function funct(arg1: mixed) { }

Any type

Same syntax.

function funct(arg1: any) { }

Define return type

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

function 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.

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

Function w/o type

Function returns void by default in both.

function funct(p1, p2) { } 

Function fat arrow

Fat arrow is a copy and paste between the two.

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

Function with callback

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

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

Funct with rest

Both support the long and short version.

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

Type Alias

Aliases are written the same way in both case.

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

Type Alias with Generic

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

type MyObject<A, B, C> = {
 property: A,
 method(val: B): C,
};
var x: MyObject<number, string, boolean> = { property: 1, method: (string) => { return
true;}}

Subtype

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

type 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.

var o: { foo?:number } = {};
o["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.

var o: { foo?:number } = {};
o["foo"] = 0;
o[“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.

Array

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

var x = [];
var y = [1, 2, 3];
var z = new Array();
var w: boolean[] = [true, false, true];
var 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.

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

Tuple

Same syntax for tuple.

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

Key interpolation

var obj1: { foo: boolean } = { foo: true };
obj1["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.

opaque type MyType = number;

Interface and Implementation

Implementing an interface is the same in Flow or TypeScript.

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

Interface as map

Both share the same syntax.

interface MyInterface {
 [key: string]: number;
}
var x: MyInterface;
x.key = 3;

Interface and object

Both are the same for the first part.

interface MyInterface{
  foo: string,
  bar: number
}
var x: MyInterface = {
  foo : "foo",
  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.

var x: MyInterface;
x.foo = "foo";
x.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.

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

Flow:

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

Contravariance

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.

interface Contravariant { writeOnly: number; }
var numberOrString = Math.random() > 0.5 ? 42 : 'forty-two';
var 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.

interface Contravariant { -writeOnly: number; }
var numberOrString = Math.random() > 0.5 ? 42 : 'forty-two';
var 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.

class Item<T> {
 prop: T;
 constructor(param: T) {
 this.prop = param;
 }
}
let 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.

interface MyInterface<A, B, C> {
 foo: A,
 bar: B,
 baz: C,
}

// Flow and TypeScript:

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

// Only TypeScript:

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

Generic Type

Both allows to have generic type in the same syntax.

type Item<T> = {
 prop: T,
};

Generic Function

Generic function share the same syntax between Flow and TypeScript.

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

Default parameter value

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

function 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.

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

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

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

Cast

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.

var x: number = otherVariable as number;
var 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: