Flow – JavaScript Type Checker

Flow – JavaScript Type Checker

By Mark Volkmann, OCI Partner & Principal Engineer

MAY 2017

Types

Types specify the conditions under which code such as a function or class will run. In static programming languages, type errors are detected at compile-time. In dynamic programming languages, type errors are typically detected at runtime. Type checking tools for dynamic languages allow detection of type errors before runtime. The most popular type checking tools for JavaScript are Flow and TypeScript.

There are many motivations for using types.

  • As stated above, type checkers can find type errors before runtime which is more convenient than waiting until runtime.
  • Types document expectations about code such as types of variables, object properties, function parameters, and function return types. Comments can be used instead, but those are more verbose, tend to be applied inconsistently, and easily go out of date when code is updated.
  • Having types increases refactoring confidence. Developers don't have to wonder what assumptions callers made about supported types.
  • Types remove the need to write error checking code for type violations.
  • Types remove the need to write type-related unit tests.
  • Editor/IDE plugins can use types to highlight issues and provide code completion.

There are also reasons to avoid using types.

  • It takes time to learn type syntax and master applying them.
  • Types make code more verbose.
  • Taking the time to specify types can hamper prototyping and rapid development. Developers can lose focus when distracted by having to satisfy a compiler or type checker.

Types provide the most benefits when:

  • The application is large, complex, or critical.
  • The expected lifetime of the code is long and refactoring is likely.
  • The code will be written and maintained by a team of developers.

It may be preferable to avoid types when the conditions above are not present.

Flow Overview

Flow is a static type checker, designed to find type errors in JavaScript programs. It is an open source tool from Facebook that is implemented using the OCaml programming language. It can catch some errors without specifying types by using type inference and flow analysis. Flow "precisely tracks the types of variables as they flow through the program." It also catches attempts to reference properties on null references.

Developers can gradually add types, but exported values in files that use Flow must have type annotations. For example, exported functions must have defined parameter and return types.

Flow supports most ES6+ features. For a list of supported JavaScript features, see https://github.com/facebook/flow/issues/560.

Flow also supports React applications and their use of JSX syntax.

TypeScript Overview

TypeScript is a competing tool from Microsoft. It is implemented in TypeScript, which is a superset of JavaScript. TypeScript files have a file extension of ".ts" instead of ".js". These files are compiled to JavaScript.

TypeScript performs both type checking and transpiling (from one version of JavaScript to another). Flow only focuses on type checking. With Flow, transpiling is typically handled by another tool such as Babel. This is good because new JS features generally land in Babel (through plugins) before TypeScript.

TypeScript has some advantages over Flow. It has a repository of type definitions for JavaScript libraries called "DefinitelyTyped". Flow has a similar repository called flow-typed, but it has far fewer entries. These type definitions allow your code to use third party JavaScript libraries that do not have type annotations and have type checking of those uses. As of April 1, 2017, DefinitelyTyped (TypeScript) had types for over 3000 packages, while flow-typed (Flow) had just over 200 and many of those are just for different versions of the same package. Flow type declaration files can be generated from TypeScript ".d.ts" files using flowgen.

Both TypeScript and Flow have editor/IDE integrations.

Flow has a goal of remaining compatible with TypeScript syntax, so switching between the two syntaxes is not a daunting task.

Installing Flow

There are many options for installing Flow in a project. These are described at https://flow.org/en/docs/install/ and also described here later.

To install Flow globally, enter "npm install -g flow-bin". To get the version that is globally installed, enter "flow version". To get help, enter "flow --help".

Running Flow

By default, Flow doesn't perform type checking on any JavaScript files. This must be enabled in each file by adding one of these comments at the top of the file: "// @flow" or "/* @flow */". To run Flow on a single file instead of all files in project, enter "flow check-contents < file-path". This doesn't require a special comment at top the file. Consider creating a script to run this command with a name like "flow1". Running Flow on all the JavaScript files in a project is described later.

Getting Type Suggestions

Flow can suggest the type annotations it thinks are appropriate for a given file. To get suggestions, enter "flow suggest file-path".

Here is an example file named untyped.js that contains no type annotations:

  1. function rectangleArea(width, height) {
  2. return width * height;
  3. }
  4.  
  5. const area = rectangleArea(100, 50);
  6. console.log('area =', area);

Here is the output from "flow suggest untyped.js":

  1. --- old
  2. +++ new
  3. @@ -1,6 +1,6 @@
  4. -function rectangleArea(width, height) {
  5. +function rectangleArea(width: number, height: number) : number{
  6. return width * height;
  7. }
  8.  
  9. -const area = rectangleArea(100, 50);
  10. +const area: number = rectangleArea(100, 50);
  11. console.log('area =', area);

Executing Code with Types

Node.js cannot directly run code that is annotated with Flow types. The simplest approach to enable this is to use the flow-node command that is included in the flow-remove-types npm package. To install it, enter "npm install -g flow-remove-types". To use it, enter "flow-node file-path".

The flow-remove-types command can be used to strip type annotations from a JavaScript file. The resulting file can then be executed with the node command. To create a new JavaScript file without types from an existing one that has types, enter "flow-remove-types --pretty file-path > new-file-path". To execute the new file, enter "node new-file-path".

Web browsers also cannot run code that contains type annotations. An approach for removing type annotations from all code in a project, rather than a single file, is described later. This is typically used for web applications.

Flow Analysis

Earlier it was stated that Flow can catch some type errors without having types specified. TypeScript is not currently as good at this.

Here is an example for which Flow catches a type error that TypeScript does not:

  1. // @flow
  2.  
  3. function product(n1, n2) {
  4. return n1 * n2;
  5. }
  6.  
  7. console.log(product(2, 'foo'));
  8. When Flow is run on this file it will produce the following output:
  9.  
  10. 4: return n1 * n2;
  11. ^^ string. The operand of an arithmetic operation
  12. must be a number.

Uninitialized References

Flow is great at detecting places where an attempt may be made to evaluate an uninitialized reference. This includes accessing object properties and calling object methods.

Here is an example:

  1. // @flow
  2.  
  3. function getLastInitial(person) {
  4. const {lastName} = person;
  5. return lastName ? lastName[0] : '';
  6. }}
  7.  
  8. const person = {
  9. firstName: 'Richard',
  10. middleName: 'Mark',
  11. lastName: 'Volkmann'
  12. };
  13. console.log(getLastInitial(person)); // good; outputs "V"
  14.  
  15. let p;
  16. console.log(getLastInitial(p)); // error

When Flow is run on this file it will produce the following output:

  1. 16: console.log(getLastInitial(p)); // error
  2. ^^^^^^^^^^^^^^^^^ function call
  3. 4: const {lastName} = person;
  4. ^^^^^^^^ property `lastName`. Property cannot
  5. be accessed on possibly undefined value
  6. 4: const {lastName} = person;
  7. ^^^^^^ uninitialized variable

Flow Types

Flow supports a wide variety of types. For more detail than what is presented here, see http://flowtype.org/docs/quick-reference.html.

Flow types are specified by appending a colon and type description to a variable, property, parameter, or function (for the return type). For example, "let score: number = 0;". In this case the type can be inferred, so the type annotation isn't necessary.

By default, the values null and undefined are not allowed by any type. To allow these, precede a type with a question mark. For example, "let score: ?number;". Flow calls this a "Maybe type".

Flow has builtin support for many types. These are defined in files that can be found in the lib directory of GitHub repo at https://github.com/facebook/flow.

The file lib/core.js defines types for all built-in JavaScript constants, functions, objects, and classes. Examples include ArrayconsoleDate*ErrorFunctionJSONMapMathPromiseRegExpSet, and String.

The file lib/dom.js defines types for the browser Document Object Model (DOM). Examples include DocumentElement,*EventHTML*ElementImageNode, and Text.

The file lib/bom.js defines types for the browser API. Examples include GeoLocationHistoryLocationNavigatorRequestResponseScreenSharedWorkerWebSocketWorker, and XMLHttpRequest.

The file lib/node.js defines types for the Node standard library. Examples include Buffereventsfshttphttps,netospathprocessquerystringstream, and url.

Basic flow types include:

  • primitives: booleannumber, and string
  • wrappers: BooleanNumber, and String (rarely used)
  • null: only matches JavaScript null value
  • void: the type of undefined and functions that don't return anything
  • specific values: a.k.a literals (rarely used)
  • any: means any type is allowed and is typically used when being too lazy to specify the proper type
  • mixed: similar to any, but must perform runtime type checks before using the value; preferred over any

Here is an example of using the mixed type along with runtime type checks:

  1. // @flow
  2.  
  3. function foo(v: mixed) {
  4. if (typeof v === 'number') return v * 2;
  5. if (typeof v === 'string') return v.length;
  6. return v;
  7. }

In JavaScript without types, passing too few arguments results in parameters being set to undefined. With Flow, an error is produced when these parameters have types that do not allow undefined. It is still okay to pass more arguments than are expected. In TypeScript this is an error.

Here's an example where Flow catches too few arguments being passed:

  1. // @flow
  2.  
  3. function product(n1: number, n2: number) {
  4. return n1 * n2;
  5. }
  6.  
  7. console.log(product(2)); // error

Here is the output that Flow produces:

  1. 7: console.log(product(2));
  2. ^^^^^^^^^^ function call
  3. 7: console.log(product(2));
  4. ^^^^^^^^^^ undefined (too few arguments,
  5. expected default/rest parameters).
  6. This type is incompatible with
  7. 3: function product(n1: number, n2: number) {
  8. ^^^^^^ number

Here are examples of basic type annotations:

  1. // @flow
  2.  
  3. function getChars(text: string, count: number, fromStart: boolean) {
  4. return fromStart ? text.substring(0, count) : text.substr(-count);
  5. }
  6.  
  7. console.log(getChars('abcdefg', 3, true)); // good; outputs 'abc'
  8. console.log(getChars('abcdefg', 3, false)); // good; outputs 'efg'
  9. console.log(getChars(3, false, 'foobar')); // error

Here is the output that Flow produces:

  1. 9: console.log(getChars(3, false, 'foobar')); // error
  2. ^ number. This type is incompatible
  3. with the expected param type of
  4. 3: function getChars(text: string, count: number, fromStart: boolean) {
  5. ^^^^^^ string
  6.  
  7. 9: console.log(getChars(3, false, 'foobar')); // error
  8. ^^^^^ boolean. This type is incompatible
  9. with the expected param type of
  10. 3: function getChars(text: string, count: number, fromStart: boolean) {
  11. ^^^^^^ number
  12.  
  13. 9: console.log(getChars(3, false, 'foobar')); // error
  14. ^^^^^^^^ string. This type is incompatible
  15. with the expected param type of
  16. 3: function getChars(text: string, count: number, fromStart: boolean) {
  17. ^^^^^^^ boolean
  18.  
  19. Found 3 errors

Type Aliases

Type aliases are useful for custom types that are used multiple times, and most are. They are often used for objects that aren't instances of a specific class. They are also used for function signatures.

The syntax for defining a type alias is: type SomeNameType = some-type;

Type alias names are not required have a suffix of "Type", but that is a common convention.

Function Parameter and Return Types

Here are examples of using type annotations for function parameter and return types:

  1. // @flow
  2.  
  3. // Result is an object type defined elsewhere.
  4. function monopoly(
  5. passGo: boolean, dice: number, piece: string): Result { ... }
  6.  
  7. // This is a type alias for a function.
  8. // They are useful when passing functions to others
  9. // and returning functions from them.
  10. // Note how => is used instead of : before the return type.
  11. type MyFnType =
  12. (passGo: boolean, dice: number, piece: string) => Result;
  13.  
  14. // Add a question mark after names of optional parameters.
  15. type DistanceFromOriginFnType =
  16. (x: number, y: number, z?: number) => number;
  17.  
  18. // Add a question mark before parameter types where
  19. // null and undefined are allowed values.
  20. type CallbackType = (err: ?Error, result?: any) => void;
  21.  
  22. // This is a type alias for a generic (a.k.a. polymorphic) function
  23. // where a type T can be specified when the type alias is used.
  24. // Generics are discussed later.
  25. type MyFnType<T> = (p1: T, p2: T) => T;

Functions that are missing ending parameters and/or the return type are considered to match function types that specify those. The following examples illustrate this:

  1. // @flow
  2.  
  3. type TestFnType = (p1: boolean, p2: number, p3: string) => void;
  4.  
  5. // Perfect match
  6. const f1: TestFnType =
  7. (a: boolean, b: number, c: string): void => {};
  8.  
  9. // Missing last parameter and return type - OK
  10. const f2: TestFnType = (a: boolean, b: number) => {};
  11.  
  12. // Missing last two parameters and return type - OK
  13. const f3: TestFnType = (a: boolean) => {};
  14.  
  15. // Missing all parameters and return type - OK
  16. const f4: TestFnType = () => {};
  17.  
  18. // Wrong type for first parameter - error
  19. const f5: TestFnType = (a: number) => {};
  20.  
  21. // Missing return type and returns wrong type - error
  22. const f6: TestFnType =
  23. (a: boolean, b: number, c: string) => 'bad';
  24.  
  25. // Wrong return type - error
  26. const f7: TestFnType =
  27. (a: boolean, b: number, c: string): string => 'bad';

Array Types

Arrays are declared with "Array" or "element-type[]". For example, "Array" is an array of Date objects. These can be nested. For example, "Array" is an array of arrays of Date objects.

Tuples are fixed-size arrays where elements at specific indexes have specific types that are not necessarily the same. For example, "type PointType = [number, number];" is an array of length two where both elements are of type number.

Here are examples of using Array types:

  1. // @flow
  2.  
  3. type ArrayOfArraysOfNumbersType = Array<Array<number>>;
  4. const aoaon: ArrayOfArraysOfNumbersType = [[1, 2], [3, 4, 5]];
  5.  
  6. type PointType = [number, number]; // a tuple
  7. // It may be more common to use an object or class
  8. // with x and y properties than to use a tuple for this.
  9. type PointArrType = Array<PointType>
  10.  
  11. function distance(p1: PointType, p2: PointType): number {
  12. return Math.hypot(p2[0] - p1[0], p2[1] - p1[1]);
  13. }
  14.  
  15. function perimiter(points: PointArrType): number {
  16. return points.reduce(
  17. (sum: number, point: PointType, index: number) =>
  18. sum += index ?
  19. distance(points[index - 1], point) : // previous point to current
  20. distance(points[points.length - 1], point), // last point to first
  21. 0);
  22. }
  23.  
  24. const points: PointArrType = [
  25. [0, 0], [3, 4], [5, 2]
  26. ];
  27. console.log('perimeter =', perimiter(points).toFixed(2)); // good
  28. console.log('perimeter =', perimiter(7)); // error

Object Types

There are three ways to specify an object type.

The first and most generic approach is to simply say the type is Object or {}. This means it can have any properties.

  1. // @flow
  2.  
  3. function logProps(obj: Object) {
  4. Object.keys(obj).forEach(key =>
  5. console.log(key, '=', obj[key]));
  6. }
  7.  
  8. logProps({foo: 1, bar: 2}); // good
  9. logProps(7); // error

The second approach is to use the name of a class or constructor function, either builtin or custom. Examples include ArrayDateErrorMapRegExp, and Set.

Here is an example that uses a custom Person class and the builtin Date class:

  1. // @flow
  2.  
  3. class Person {
  4. // These are declarations of class properties
  5. // that are in each instance.
  6. name: string;
  7. birthday: Date;
  8. height: number;
  9. spouse: Person;
  10. // Class properties are optional and there isn't a way to make them required.
  11.  
  12. constructor(name: string, birthday: Date, height: number): void {
  13. this.name = name;
  14. this.birthday = birthday;
  15. this.height = height;
  16. }
  17.  
  18. marry(person: Person): void {
  19. this.spouse = person;
  20. person.spouse = this;
  21. }
  22. }
  23.  
  24. const tami: Person = new Person('Tami', new Date(1961, 8, 9), 65);
  25. const mark: Person = new Person('Mark', new Date(1961, 3, 16), 74);
  26. tami.marry(mark);
  27.  
  28. function logPerson(person: Person): void {
  29. const status: string = person.spouse ?
  30. 'married to ' + person.spouse.name : 'single';
  31. console.log(person.name + ' is ' + status + '.');
  32. }
  33.  
  34. logPerson(mark); // good
  35. logPerson(new Date()); // error

The third approach is to describe the properties the object can have. Matching objects can have additional properties. The specified properties are required by default. To make them optional, add ? after their names.

  1. // @flow
  2.  
  3. type PersonType = {
  4. name: string,
  5. birthday: Date,
  6. spouse?: ?PersonType // preceding a type with ? allows null and undefined
  7. // The height property was purposely omitted to show that
  8. // objects of this type can have additional properties.
  9. };
  10.  
  11. const tami: PersonType = {
  12. name: 'Tami',
  13. birthday: new Date(1961, 8, 9),
  14. height: 65
  15. };
  16.  
  17. const mark: PersonType = {
  18. name: 'Mark',
  19. birthday: new Date(1961, 3, 16),
  20. height: 74,
  21. spouse: tami
  22.  
  23. tami.spouse = mark;
  24.  
  25. function logPerson(person: PersonType): void {
  26. const status = person.spouse ?
  27. 'married to ' + person.spouse.name : 'single';
  28. console.log(`${person.name} is ${status}.`);
  29. }
  30.  
  31. logPerson(mark); // Mark is married to Tami.

When objects are used as maps, they can have any string keys and all values are often the same type. There is a special syntax for this type of object that uses square brackets in the type description.

  1. // @flow
  2.  
  3. type PlayerToNumberMapType = {
  4. [player: string]: number // "player" just serves as a description of the keys
  5. };
  6.  
  7. const playerToNumberMap: PlayerToNumberMapType = {
  8. 'Mario Lemieux': 66,
  9. 'Wayne Gretzky': 99
  10. };
  11.  
  12. Object.keys(playerToNumberMap).forEach(player => {
  13. const number = playerToNumberMap[player];
  14. console.log(`${player} is number ${number}`);
  15. });

Class Types

Class types are an advanced feature of Flow. They allow a variable to hold a reference to a class. Such variables can be used later to create an instance of the class. Here is an example:

  1. // @flow
  2.  
  3. // These classes could be more involved.
  4. class Animal {}
  5. class Mineral {}
  6. class Vegetable {}
  7.  
  8. type AMVType = Animal | Mineral | Vegetable;
  9.  
  10. const alive = false;
  11. const grows = false;
  12. const clazz: Class<AMVType> = alive ? Animal : grows ? Vegetable : Mineral;
  13.  
  14. const thing: AMVType = new clazz();
  15. console.log(thing instanceof Mineral); // true

Generics

Generics are used to specify parameterized types where the types used within a custom type can be specified. They can be used in definitions of classes, interfaces, functions, and in type aliases for objects.

Typically the type parameter name is T, but any name can be used and there can be more than one type parameter, separated by commas.

Here are examples of generics:

  1. class MyClass<T> {
  2. // Use T in the definitions of properties and/or methods.
  3.  
  4. someProp: T;
  5.  
  6. constructor(someProp: T) {
  7. this.someProp = someProp;
  8. }
  9.  
  10. getSomeProp(): T {
  11. return this.someProp;
  12. }
  13. }
  14.  
  15. const myObj: MyClass<number> = new MyClass(3);
  16. console.log(myObj.getSomeProp()); // 3
  17.  
  18. type PricedType<T> = {
  19. item: T, price: number, date: Date
  20. };
  21.  
  22. function logPricedType<T>(priced: PricedType<T>) {
  23. console.log(String(priced.item),
  24. 'cost', priced.price,
  25. 'on', priced.date.toDateString());
  26. }
  27.  
  28. const apple: PricedType<string> = {
  29. item: 'Gala apple',
  30. price: 0.99,
  31. date: new Date()
  32. };
  33. logPricedType(apple); // Gala apple cost 0.99 on Fri Apr 07 2017
  34.  
  35. class Fruit {
  36. kind: string;
  37. constructor(kind: string): void {
  38. this.kind = kind;
  39. }
  40. toString(): string {
  41. return this.kind;
  42. }
  43. }
  44. const banana: PricedType<Fruit> = {
  45. item: new Fruit('Chiquita banana'),
  46. price: 0.29,
  47. date: new Date()
  48. };
  49. logPricedType(banana); // Chiquita banana cost 0.29 on Fri Apr 07 2017

Here is an example of using generics with the builtin Map and Set classes:

  1. // @flow
  2.  
  3. type PlayerType = {
  4. name: string,
  5. number: number,
  6. position: string
  7. };
  8.  
  9. const gretzky: PlayerType =
  10. {name: 'Wayne Gretzky', number: 99, position: 'center'};
  11. const lemieux: PlayerType =
  12. {name: 'Mario Lemieux', number: 66, position: 'center'};
  13.  
  14. const players: PlayerType[] = [gretzky, lemieux];
  15. const playerSet: Set<PlayerType> = new Set(players);
  16. const playerMap: Map<number, PlayerType> = new Map();
  17.  
  18. for (const player of players) {
  19. playerMap.set(player.number, player);
  20. }
  21.  
  22. console.log('map-set.js: playerSet =', playerSet);
  23. console.log('map-set.js: playerMap =', playerMap);

The output from this code is:

  1. map-set.js: playerSet = Set {
  2. { name: 'Wayne Gretzky', number: 99, position: 'center' },
  3. { name: 'Mario Lemieux', number: 66, position: 'center' } }
  4. map-set.js: playerMap = Map {
  5. 99 => { name: 'Wayne Gretzky', number: 99, position: 'center' },
  6. 66 => { name: 'Mario Lemieux', number: 66, position: 'center' } }

Interfaces

Interfaces are used to describe commonality between classes that isn't expressed through a common superclass. This includes properties and methods. If you have the ability to modify the classes, it may be best to give them a common superclass.

A class can specify the interfaces it implements using the "implements" keyword. Any number of comma-separated interface names can follow this.

Here is an example of using an interface:

  1. interface Vehicle {
  2. start(): void,
  3. stop(): void
  4. }
  5.  
  6. class Boat implements Vehicle {
  7. start(): void { console.log('The boat is started.'); }
  8. stop(): void { console.log('The boat is stopped.'); }
  9. }
  10.  
  11. class Car implements Vehicle {
  12. start(): void { console.log('The car is started.'); }
  13. stop(): void { console.log('The car is stopped.'); }
  14. }
  15.  
  16. class House {} // has no methods
  17.  
  18. function testDrive(vehicle: Vehicle) {
  19. vehicle.start();
  20. vehicle.stop();
  21. }
  22.  
  23. const boat: Vehicle = new Boat();
  24. testDrive(boat); // good
  25.  
  26. const car: Vehicle = new Car();
  27. testDrive(car); // good
  28.  
  29. const house: Vehicle = new House();
  30. testDrive(house); // error, not a Vehicle

Unions

Unions specify that a value can have one of a list of types. Here are examples of using unions.

  1. type PrimitiveType = boolean | number | string;
  2. let value: PrimitiveType = true;
  3. value = 7; // good
  4. value = 'foo'; // good
  5. value = {}; // error
  6.  
  7. // These are object types with a specific value for their "type" property.
  8. type AnimalType = {name: string, type: 'animal'};
  9. type MineralType = {name: string, type: 'mineral'};
  10. type VegetableType = {name: string, type: 'vegetable'};
  11. type ThingType = AnimalType | MineralType | VegetableType;
  12.  
  13. const dog: AnimalType = {name: 'Dasher', type: 'animal'};
  14. const mineral: MineralType = {name: 'amethyst', type: 'mineral'};
  15. const vegetable: VegetableType = {name: 'corn', type: 'vegetable'};
  16.  
  17. let thing: ThingType = dog; // good
  18. console.log(thing.name); // Dasher
  19. thing = mineral; // good
  20. console.log(thing.name); // amethyst
  21. thing = vegetable; // good
  22. console.log(thing.name); // corn
  23. thing = {name: 'bad', type: 'other'}; // error

Unions can be used for enums. Here is an example:

  1. type ActivityType = 'swim' | 'bike' | 'run';
  2. // In TypeScript this is called a "String Literal Type".
  3.  
  4. let activity: ActivityType = 'swim'; // good
  5. console.log('Your current activity is', activity);
  6.  
  7. activity = 'bike'; // good
  8. console.log('Your current activity is', activity);
  9.  
  10. activity = 'run'; // good
  11. console.log('Your current activity is', activity);
  12.  
  13. activity = 'collapse'; // error

Sharing Type Aliases

Type aliases defined in one file can be used in others. To share a type, export it with "export type". To use a type defined in another file, import it with "import type". This feature requires the use of a module bundler like Webpack.

Here is the content of a file named type-alias-export.js that exports a type alias and a function:

  1. // @flow
  2.  
  3. // This type matches any object that has
  4. // a "name" property with a type of string.
  5. export type NamedType = {name: string};
  6.  
  7. export function sayHello(thing: NamedType): void {
  8. console.log('Hello, ' + thing.name + '!');
  9. }

Here is the content of a file named type-alias-import.js that imports the type alias and function:

  1. // @flow
  2.  
  3. import type {NamedType} from './type-alias-export';
  4. import {sayHello} from './type-alias-export';
  5.  
  6. const mark: NamedType = {name: 'Mark', hobby: 'running'};
  7. sayHello(mark); // good
  8. sayHello({name: 'Tami', hobby: 'swimming'}); // good
  9. sayHello('Mark'); // error

Escape Hatch

Sometimes, but not often, Flow can't be easily satisfied. To disable Flow type checking for a single line, precede the line with this comment:

// $FlowFixMe optional description of why

Flow Server

The Flow server is the brains behind Flow. It analyzes and stores many things about the flow of code in an application. This includes variable/function types, locations of their definitions, and references to them.

When a Flow server is started, it creates several “flow” processes, based on # of cores and the server.max_workers option. These are used to perform parallel evaluation of multiple files in the background for performance.

Flow considers all .js files under the directory containing the file .flowconfig unless other directories are specified in this file. Creating a .flowconfig file is discussed later. Initially it checks all of these files. After that is completed, Flow only checks files that have changed, files that import from files that have changed, and newly created files.

The Flow server doesn't output error messages. It just collects information about the code and type error messages. Then it waits for requests to be made using the Flow command-line interface (CLI).

To start a Flow server in the background, enter "flow start". To start a Flow server in the foreground, enter "flow server". Typically "start" is preferred.

To output errors collected by the server, enter "flow status" or just "flow". If a Flow server is not running, these commands start it and run a full check.

To check all files from scratch and output errors, enter "flow check". If a Flow server isn't currently running, this will start one and then stop it when finished checking.

To stop a Flow server, enter "flow stop" from a directory in the project where it is running. An alternative in *nix-based systems is to enter "killall flow".

The server must be stopped and restarted after changes to .flowconfig or declaration files (described later) because the server caches those.

Flow provides CLI commands that can be used by editors/IDEs to obtain the information it has collected, including type errors. Many of the commands take a file path, line number, and column number (referred to below as an "FLC") to identify a variable or function of interest and output a list of the same. The column number can be any column within the name of a variable or function, not just beginning.

Flow CLI commands that query the server are intended to be used from editor plugins. They include:

  • autocomplete - This inserts a "magic autocomplete token" at the specified position in an editor/IDE.
  • coverage - This outputs the percentage of expressions in a given file for which Flow knows their types.
    To use this, enter "flow coverage path-to-js-file".
  • find-refs - This outputs FLCs that refer to the variable at a given FLC.
    To use this, enter "flow get-def path-to-js-file line-num col-num".
  • gen-flow-files - This generates a .js.flow file (described later) containing type declarations for a given .js file.
    To use this, enter "flow gen-flow-files path-to-js-file > some-name.js.flow".
  • get-def - This gets FLCs for the beginning and end of a name in the definition of a variable at a given FLC.
    To use this, enter "flow get-def path-to-js-file line-num col-num".
  • get-imports - This gets FLCs of each imported module in a given file.
    To use this, enter "flow get-imports path-to-js-file".
  • type-at-pos - This gets the type of a variable at an FLC and FLCs for all references to it.
    To use this, enter "flow type-at-pos path-to-js-file line-num col-num".

In the future, Flow may add CLI commands to perform code refactorings using its knowledge of all the code in an application.

Using Flow in a Project

Here is a checklist of common tasks to prepare for using Flow in a project:

  1. Install "dev dependencies".
  2. Add npm scripts to package.json.
  3. Setup ESLint.
  4. Setup Babel.
  5. Create a .flow-config file.
  6. Use flow-typed to get dependency type declarations.
  7. Configure editors/IDEs to use Flow.

Each of these tasks is described in the following sections.

The example that follows is a Node.js project that uses CommonJS modules. A web UI project that uses ES Modules would need a module bundler like Webpack or Rollup. Configuring that is beyond the scope of this article. If targeting React, consider using create-react-app which configures Webpack and much more for you.

Dev Dependencies

"cd" to the top project directory containing package.json. If that file doesn't exist yet, enter "npm init" to create it.

For each of the following, enter "npm install -D name" or "yarn add -D name".

  • babel-cli - command-line interface to the Babel transpiler
  • babel-eslint - alternate parser for ESLint that understands ES6+ syntax
  • babel-plugin-transform-flow-strip-types - removes Flow type annotations from Babel output
  • babel-preset-env - automatically determines needed Babel plugins and polyfills based on a target environment
  • eslint - the most popular JavaScript linter
  • eslint-plugin-flowtype - implements ESLint rules to check usage of Flow types
  • flow-bin - the Flow type checker
  • flow-watch - "file watcher that clears the console (terminal) and runs flow on each change"
  • npm-run-all - "runs multiple npm-scripts in parallel or sequentially"

When using create-react-app many of these are installed for you. In that case, only babel-cli, flow-bin, flow-watch, and npm-run-all need to be installed in order to use the npm scripts described next.

npm Scripts

Here are some recommended npm scripts that can be added to the "scripts" section in package.json.

  • "babel": "babel src -d build"
    This transpiles all .js files under the src directory into the build directory.
  • "flow": "flow"
    This runs flow on all .js files in the project or only those specified in .flowconfig (described later).
  • "floww": "flow-watch"
    This is the same as the flow script, but it keeps running, watching files for changes.
  • "lint": "eslint --quiet src"
    This runs eslint on all .js files under the src directory.
  • "run": "node build/index.js"
    This runs the transpiled version of the application.
  • "start": "npm-run-all lint flow babel run"
    This combines previous steps

ESLint Setup

Create the file .eslintrc.json in the top project directory. In addition to configuring the standard ESLint rules, considering adding the following rule configurations that check the use of Flow types. Modify them based on your preferences.

  1. "flowtype/boolean-style": ["error", "boolean"],
  2. "flowtype/define-flow-type": ["error", {"no-undef": "error"}],
  3. "flowtype/delimiter-dangle": ["error", "never"],
  4. "flowtype/generic-spacing": ["error", "never"],
  5. "flowtype/no-dupe-keys": "error",
  6. "flowtype/no-primitive-constructor-types": "error",
  7. "flowtype/no-weak-types": "warn",
  8. "flowtype/object-type-delimiter": ["error", "comma"],
  9. "flowtype/require-parameter-type": "off",
  10. "flowtype/require-return-type": "off",
  11. "flowtype/require-valid-file-annotation": "off",
  12. "flowtype/semi": ["error", "always"],
  13. "flowtype/sort-keys": "off",
  14. "flowtype/space-after-type-colon": ["error", "always"],
  15. "flowtype/space-before-generic-bracket": ["error", "never"],
  16. "flowtype/space-before-type-colon": ["error", "never"],
  17. "flowtype/type-id-match": "error",
  18. "flowtype/union-intersection-spacing": ["error", "always"],
  19. "flowtype/use-flow-type": "error",
  20. "flowtype/valid-syntax": "error",

For descriptions of these rules, see https://github.com/gajus/eslint-plugin-flowtype#eslint-plugin-flowtype-rules.

Babel Setup

In order to execute code that contains Flow type annotations, the type annotations must be removed. Babel can do this using the transform-flow-strip-types plugin.

To configure Babel to use this plugin, create the file .babelrc in the top project directory with the following content:

  1. {
  2. "presets": [
  3. ["env", {
  4. "targets": {
  5. "node": 7.7
  6. }
  7. }]
  8. ],
  9. "plugins": [
  10. "transform-flow-strip-types"
  11. ]
  12. }

A different "targets" value would be used for a web app and a different Node version can be used for a Node app.

flow-typed

flow-typed is a tool and "a central repository for Flow library definitions" found at https://github.com/flowtype/flow-typed. Examples of libraries for which type definitions exist include Axios, Chalk, Enzyme, Express, Jasmine, Jest, Lodash, Moment, pg (a Postgresql library), react-redux, React Router, Redux, and RxJS.

To use library definitions from flow-typed in a project:

  1. Install the tool with "npm install -g flow-typed".
  2. "cd" to the top project directory.
  3. Enter "flow-typed install". This creates a flow-typed directory if it isn't already present. It then installs type declarations there for all dependencies found in package.json. For dependencies that do not yet have type declaration files in the flow-typed repository, it generates "stubs" which use the "any" type for everything.
  4. Add the flow-typed directory to version control for the project.

If the flow-typed command prompts for your GitHub username and password, you can press enter without supplying them.

To update previously installed type definitions, enter "flow-typed update".

.flowconfig FILE

This file configures the use of Flow in a project. To create this file, cd to the top project directory and enter "flow init". This will create a .flowconfig file that is empty except for four section names. Comment lines in this file start with # or ;, optionally preceded by whitespace.

Here is an example .flowconfig file:

# This section lists directories and/or files to ignore.
[ignore]
<PROJECT_ROOT>/node_modules
 
# This section lists directories and/or files to check.
# The top project directory is included by default.
[include]
 
# This section lists directories and/or files
# that contain library declaration files.
# By default Flow with look in the directory named "flow-typed".
[libs]
 
# This section configures Flow options described
# at https://flow.org/en/docs/config/options/.
[options]

It is essential to have a .flowconfig file in the project root directory. Flow searches upward until this file is found. If it reaches the top directory without finding one, Flow will check every JavaScript file below that, which will heavily tax your computer. If this happens, kill the Flow server by entering "flow stop" or "killall flow" (on *nix systems).

For more information on the .flowconfig file, see https://flow.org/en/docs/config/.

Editor/IDE Setup

Flow plugins exist for many editors and IDEs. Some perform type checking during editing, while others only check when files are saved. Here is a summary of the most popular plugins:

  1. let g:ale_linters = {
  2. \ 'javascript': ['eslint', 'flow'],
  3. \}


Option #2 is the vim-flow plugin at https://github.com/flowtype/vim-flow. This provides several commands. The most useful are :FlowType to display the type of the variable under the cursor and :FlowJumpToDef to jump to the definition of the variable under the cursor. This also adds object property and method completions using “Omni completion” which must be enabled. To trigger, press c-x c-o. Then move up and down in the list of completions with tab/shift-tab, c-n/c-p, or down/up arrows. Continue typing to use selection.

It is beneficial to use both ALE and vim-flow.

Sample Project

Let's walk through the use of Flow in a sample project. It uses a popular npm package (Lodash) and one that is less known (liner). The application reads lines from a text file using liner, uses the Lodash function startCase to capitalize each word, outputs each line, and outputs the number of lines read.

To install the application dependencies (not dev dependencies), cd to the top project directory and enter "npm install -S name" or "yarn add name" for each of these:

  • liner - "reads lines from files and streams"
  • lodash - "modern JavaScript utility library delivering modularity, performance & extras"

The application consists of three files. Note the use of Flow types in this code.

The file src/index.js is the starting point of the app. It imports the filer function from the file src/filer.js and asks it to read from the file haiku.txt.

  1. // @flow
  2. import filer from './filer';
  3.  
  4. filer('./haiku.txt', (lineCount: number) => {
  5. console.log('line count is', lineCount);
  6. });

The file src/filer.js uses the Liner class from the liner npm package and the startCase function from the lodash package.

  1. // @flow
  2. const Liner = require('liner');
  3. const _ = require('lodash/string');
  4.  
  5. /**
  6.  * Outputs each line in the text file at the given path,
  7.  * capitalizing the first letter of each word,
  8.  * and calls cb with the number of lines read.
  9.  */
  10. function processFile(path: string, cb: (number) => void): void {
  11. let count = 0;
  12. const liner = new Liner(path);
  13.  
  14. liner.on('readable', () => {
  15. while (true) {
  16. const line = liner.read();
  17. if (line === null) break;
  18. console.log(_.startCase(line));
  19. count++;
  20. }
  21. });
  22.  
  23. liner.on('end', () => cb(count));
  24.  
  25. liner.on('error', err => console.error(err));
  26. }
  27.  
  28. export default processFile;

haiku.txt

Out of memory.
We wish to hold the whole sky,
But we never will.

Here is the file package.json that describes the dependencies and scripts used by the application:

  1. {
  2. "name": "sample-project",
  3. "version": "1.0.0",
  4. "dependencies": {
  5. "liner": "^0.3.3",
  6. "lodash": "^4.17.3"
  7. },
  8. "devDependencies": {
  9. "babel-cli": "^6.24.0",
  10. "babel-eslint": "^7.2.1",
  11. "babel-plugin-transform-flow-strip-types": "^6.22.0",
  12. "babel-preset-env": "^1.2.2",
  13. "eslint": "^3.18.0",
  14. "eslint-plugin-flowtype": "^2.30.4",
  15. "flow-bin": "^0.42.0",
  16. "flow-watch": "^1.1.1",
  17. "npm-run-all": "^4.0.2"
  18. },
  19. "scripts": {
  20. "babel": "babel src -d build",
  21. "flow": "flow",
  22. "floww": "flow-watch",
  23. "lint": "eslint --quiet src",
  24. "run": "node build/index.js",
  25. "start": "npm-run-all lint flow babel run"
  26. }
  27. }

To run ESLint, Flow, Babel, and the application, enter "npm start".

Here is the output of the application. Note how all the words are capitalized and the number of lines read is output at the end.

Out Of Memory
We Wish To Hold The Whole Sky
But We Never Will
line count is 3

React/JSX Support

React supports two ways of defining components, class-based and stateless functional. In stateless functional components, Flow can be used to specify the types of props obtained through destructuring of the props object. In class-based components, Flow can be used to specify the types of props, default props, and state.

In a stateless functional component, declare the type of this.props, not types within a destructuring of it.

This does not work:

const MyComponent = ({foo: string, bar: number}) => { ... };

This does work:

const MyComponent = ({foo, bar}: {foo: string, bar: number}) => { ... };

This works and is more readable:

type PropsType = {foo: string, bar: number};
const MyComponent = ({foo, bar}: PropsType) => { ... };

In a class component, declare props, state, and methods as "public class fields".

For more information on React support, see https://flowtype.org/docs/react.html.

Built-in React Types

While Flow is not specific to React, it has built-in support for React types including:

  • Component class, including lifecycle methods
  • Element class, whose instances are typically created using JSX
  • PropTypes object type which includes properties for declaring the types of component props in the React way includinganyarray, arrayOf, boolelement, func, instanceofnode, numberobjectobjectOfoneOf,oneOfTypeshape, and string
  • react module which includes createElementrenderToString, and more
  • react-dom module which includes findDOMNode and more
  • SyntheticEvent and subclasses including SyntheticDragEventSyntheticInputEventSyntheticKeyboardEvent, SyntheticMouseEventSyntheticTouchEventSyntheticWheelEvent, and more

Flow versus React PropTypes

There are benefits to using Flow types in place of React PropTypes, but there are also downsides. Flow types allow type errors in props to be detected in Flow-aware editors. Flow can also be run as part of the build process and a build can be aborted if any errors are found. React PropTypes allow errors to be flagged in tests and when the app is run by displaying messages in the browser console.

It is possible to specify types for props using both Flow types and React PropTypes to get both sets of benefits. However, I suspect that many React developers will gradually shift to only using Flow types for props because it is tedious to specify the types of the props in two ways and keep them in sync when changes are needed.

Library Definitions

Library definitions declare the types of globals (variables, functions, and classes) and modules (CommonJS or ES) without modifying the code in which they are defined. They are used when definitions cannot be modified to add type declarations. One example is a library like Lodash. This allows usages of such libraries to be type-checked.

Flow types can be specified in four locations.

  1. In source files. This is the preferred location when source files can be modified.
  2. In type declaration files in the flow-typed directory of the project. This is the preferred location when source files cannot be modified.
  3. In type declaration files a directory specified in the "include" section of the .flowconfig file. Typically the flow-typed directory should be used instead.
  4. In a .js.flow file in the same directory as the source file whose types it defines. For example, types for the file foo.js can be specified in a file named foo.js.flow.

Kinds of Declarations

There are five kinds of declarations that can be specified. Note that these declarations use Flow-specific syntax, not JavaScript syntax.

Variables
declare var name: type;
Functions
declare function name(p1-name: p1-type, ...) return-type;
Classes and Interfaces
declare [class|interface] class-name {
  constructor(p1-name: p1-type): class-name;
  static method-name(p1-name: p1-type, ...): return-type;
  method-name(p1-name: p1-type, ...): return-type;
}
Flow Types
declare type name = type;
Modules
These are covered in the next section because there is much more to say about them than the previous kinds of declarations.

Modules

Modules provide a named scope for variables, functions, classes, types, and interfaces. Otherwise, those kinds of declarations describe things that are global. Modules are defined in a file with the same name as module they describe.

Flow supports two kinds of JavaScript modules, CommonJS and ECMAScript (ES).

Here is an example of a simple module defined using CommonJS syntax:

  1. function double(n) {
  2. return n * 2;
  3. }
  4.  
  5. exports.double = double;

Here is an example of code that uses this module:

  1. // @flow
  2. const math = require('./math');
  3.  
  4. console.log(math.double(3)); // good
  5. console.log(math.double('bad')); // error

Here is a Flow declaration file for this CommonJS module:

  1. declare module './math' {
  2. # type declarations go here
  3. declare module.exports: {
  4. double(n: number): number;
  5. }
  6. }

Here is an example of the same simple module defined using ES module syntax:

  1. export function double(n) {
  2. return n * 2;
  3. }

Here is an example of code that uses this module:

  1. // @flow
  2. import * as math from './math';
  3.  
  4. console.log(math.double(3)); // good
  5. console.log(math.double('bad')); // error

Here is a Flow declaration file for this ES module:

  1. declare module './math' {
  2. # type declarations go here
  3. declare export function double(n: number): number;
  4. }

.js.flow DECLARATIONS

This kind of declaration file is colocated with the implementation file. For example, if the implementation file is math.js then math.js.flow should reside in the same directory. These files must declare types for anything that is exported. Types declared in .js.flow files are used in place of those in the corresponding .js file, if any. This is useful when .js files do not include type annotations or they are incorrect and cannot be modified. This also supports keeping .js files free of Flow-specific syntax which allows them to be used without tooling to strip out type annotations.

Here is an example of a .js.flow file named math.js.flow. It can be used to supply types for either of the preceding math.js files.

function double(n: number): number {}

Functions defined in these files do not have to include implementations in their bodies, but they do at least need an empty body.

For more details on creating library defintions, see https://flow.org/en/docs/libdefs/creation/.

Summary

For developers that have primarily worked in dynamic programming languages, it is natural to suspect that the benefits derived from types might not justify the extra work required to specify them. In my experience with Flow I have been surprised at how often adding types uncovered issues in existing code. I highly recommend giving Flow, or TypeScript, a try! Start simple, perhaps just adding types for function parameters and return types. Over time I believe you will notice that your confidence in the quality of the code will increase and the number of errors you discover at runtime will decrease!

Many thanks go the reviewers of this article that included Lance Finney (OCI) and Curt Schneider (MasterCard).

Please send feedback on this article to mark@objectcomputing.com.

Resources

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.