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.
Declaration
TypeScript and Flow are exactly the same.
1Const2let3var
Primitive
Again, no difference.
1Number2Boolean3String4Null5Undefined6Void
Array
And again, no difference.
1[]2Array<T>
Enum
TypeScript is shorted:
1// Language file:2export enum Language {3 English,4 French5}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 }14}
Flow requires the value in const, and special import. It's more verbose.
1// Language file:2// @flow3export const LANGUAGES_VALUE = {4 FRENCH: "French",5 ENGLISH: "English"6};
1export type Language = $Values<typeof LANGUAGES_VALUE>;2// Use of Language file:3import type { Language } from "../general/Language";4import { LANGUAGES_VALUE } from "../general/Language";56export class LocaleManager {7 static GetLocale(language: Language): void {8 switch (language) {9 // ...10 }11 }12}
Maybe Argument
Watch out the question mark in the few next examples. Flow distinguishes null and undefined between optional and maybe.
1TypeScript:23{ propertyName?: string } //Allow null & undefined
Flow:
1{ propertyName: ?string } //Allow null & undefined
Optional Property
TypeScript:
1{ propertyName?: string } //Allow null & undefined
Flow:
1{ propertyName?: string } //Allow undefined
Optional Argument
TypeScript:
1( propertyName?: string ) //Allow null & undefined
Flow:
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// or3type 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,4};5var x: MyObject<number, string, boolean> = { property: 1, method: (string) => { return6true;}}
Subtype
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.
Array
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];
Tuple
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;3}4class Foo implements Serializable {5 serialize() { return '[Foo]'; }6}
Interface as map
Both share the same syntax.
1interface MyInterface {2 [key: string]: number;3}4var x: MyInterface;5x.key = 3;
Interface and object
Both are the same for the first part.
1interface MyInterface{2 foo: string,3 bar: number4}5var x: MyInterface = {6 foo : "foo",7 bar : 18};
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: string3}4var x: MyInterface = { foo: "bar" };5x.foo = "123"; // Blocked!
Flow:
1interface MyInterface{2 +foo: string3}4var x: MyInterface = { foo: "bar" };5x.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.
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 }6}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,5}
// 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,3};
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,3};
Flow uses the semicolon to enforce the generic type and equal sign for default.
1type Item<T: number = 1> = {2 prop: T,3};
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.
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: