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:
- function rectangleArea(width, height) {
- return width * height;
- }
-
- const area = rectangleArea(100, 50);
- console.log('area =', area);
Here is the output from "flow suggest untyped.js
":
- --- old
- +++ new
- @@ -1,6 +1,6 @@
- -function rectangleArea(width, height) {
- +function rectangleArea(width: number, height: number) : number{
- return width * height;
- }
-
- -const area = rectangleArea(100, 50);
- +const area: number = rectangleArea(100, 50);
- 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:
- // @flow
-
- function product(n1, n2) {
- return n1 * n2;
- }
-
- console.log(product(2, 'foo'));
- When Flow is run on this file it will produce the following output:
-
- 4: return n1 * n2;
- ^^ string. The operand of an arithmetic operation
- 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:
- // @flow
-
- function getLastInitial(person) {
- const {lastName} = person;
- return lastName ? lastName[0] : '';
- }}
-
- const person = {
- firstName: 'Richard',
- middleName: 'Mark',
- lastName: 'Volkmann'
- };
- console.log(getLastInitial(person)); // good; outputs "V"
-
- let p;
- console.log(getLastInitial(p)); // error
When Flow is run on this file it will produce the following output:
- 16: console.log(getLastInitial(p)); // error
- ^^^^^^^^^^^^^^^^^ function call
- 4: const {lastName} = person;
- ^^^^^^^^ property `lastName`. Property cannot
- be accessed on possibly undefined value
- 4: const {lastName} = person;
- ^^^^^^ 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 Array
, console
, Date
, *Error
, Function
, JSON
, Map
, Math
, Promise
, RegExp
, Set
, and String
.
The file lib/dom.js
defines types for the browser Document Object Model (DOM). Examples include Document
, Element
,*Event
, HTML*Element
, Image
, Node
, and Text
.
The file lib/bom.js
defines types for the browser API. Examples include GeoLocation
, History
, Location
, Navigator
, Request
, Response
, Screen
, SharedWorker
, WebSocket
, Worker
, and XMLHttpRequest
.
The file lib/node.js
defines types for the Node standard library. Examples include Buffer
, events
, fs
, http
, https
,net
, os
, path
, process
, querystring
, stream
, and url
.
Basic flow types include:
- primitives:
boolean
,number
, andstring
- wrappers:
Boolean
,Number
, andString
(rarely used) null
: only matches JavaScript null valuevoid
: the type ofundefined
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 typemixed
: similar toany
, but must perform runtime type checks before using the value; preferred overany
Here is an example of using the mixed
type along with runtime type checks:
- // @flow
-
- function foo(v: mixed) {
- if (typeof v === 'number') return v * 2;
- if (typeof v === 'string') return v.length;
- return v;
- }
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:
- // @flow
-
- function product(n1: number, n2: number) {
- return n1 * n2;
- }
-
- console.log(product(2)); // error
Here is the output that Flow produces:
- 7: console.log(product(2));
- ^^^^^^^^^^ function call
- 7: console.log(product(2));
- ^^^^^^^^^^ undefined (too few arguments,
- expected default/rest parameters).
- This type is incompatible with
- 3: function product(n1: number, n2: number) {
- ^^^^^^ number
Here are examples of basic type annotations:
- // @flow
-
- function getChars(text: string, count: number, fromStart: boolean) {
- return fromStart ? text.substring(0, count) : text.substr(-count);
- }
-
- console.log(getChars('abcdefg', 3, true)); // good; outputs 'abc'
- console.log(getChars('abcdefg', 3, false)); // good; outputs 'efg'
- console.log(getChars(3, false, 'foobar')); // error
Here is the output that Flow produces:
- 9: console.log(getChars(3, false, 'foobar')); // error
- ^ number. This type is incompatible
- with the expected param type of
- 3: function getChars(text: string, count: number, fromStart: boolean) {
- ^^^^^^ string
-
- 9: console.log(getChars(3, false, 'foobar')); // error
- ^^^^^ boolean. This type is incompatible
- with the expected param type of
- 3: function getChars(text: string, count: number, fromStart: boolean) {
- ^^^^^^ number
-
- 9: console.log(getChars(3, false, 'foobar')); // error
- ^^^^^^^^ string. This type is incompatible
- with the expected param type of
- 3: function getChars(text: string, count: number, fromStart: boolean) {
- ^^^^^^^ boolean
-
- 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:
- // @flow
-
- // Result is an object type defined elsewhere.
- function monopoly(
- passGo: boolean, dice: number, piece: string): Result { ... }
-
- // This is a type alias for a function.
- // They are useful when passing functions to others
- // and returning functions from them.
- // Note how => is used instead of : before the return type.
- type MyFnType =
- (passGo: boolean, dice: number, piece: string) => Result;
-
- // Add a question mark after names of optional parameters.
- type DistanceFromOriginFnType =
- (x: number, y: number, z?: number) => number;
-
- // Add a question mark before parameter types where
- // null and undefined are allowed values.
- type CallbackType = (err: ?Error, result?: any) => void;
-
- // This is a type alias for a generic (a.k.a. polymorphic) function
- // where a type T can be specified when the type alias is used.
- // Generics are discussed later.
- 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:
- // @flow
-
- type TestFnType = (p1: boolean, p2: number, p3: string) => void;
-
- // Perfect match
- const f1: TestFnType =
- (a: boolean, b: number, c: string): void => {};
-
- // Missing last parameter and return type - OK
- const f2: TestFnType = (a: boolean, b: number) => {};
-
- // Missing last two parameters and return type - OK
- const f3: TestFnType = (a: boolean) => {};
-
- // Missing all parameters and return type - OK
- const f4: TestFnType = () => {};
-
- // Wrong type for first parameter - error
- const f5: TestFnType = (a: number) => {};
-
- // Missing return type and returns wrong type - error
- const f6: TestFnType =
- (a: boolean, b: number, c: string) => 'bad';
-
- // Wrong return type - error
- const f7: TestFnType =
- (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:
- // @flow
-
- type ArrayOfArraysOfNumbersType = Array<Array<number>>;
- const aoaon: ArrayOfArraysOfNumbersType = [[1, 2], [3, 4, 5]];
-
- type PointType = [number, number]; // a tuple
- // It may be more common to use an object or class
- // with x and y properties than to use a tuple for this.
- type PointArrType = Array<PointType>
-
- function distance(p1: PointType, p2: PointType): number {
- return Math.hypot(p2[0] - p1[0], p2[1] - p1[1]);
- }
-
- function perimiter(points: PointArrType): number {
- return points.reduce(
- (sum: number, point: PointType, index: number) =>
- sum += index ?
- distance(points[index - 1], point) : // previous point to current
- distance(points[points.length - 1], point), // last point to first
- 0);
- }
-
- const points: PointArrType = [
- [0, 0], [3, 4], [5, 2]
- ];
- console.log('perimeter =', perimiter(points).toFixed(2)); // good
- 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.
- // @flow
-
- function logProps(obj: Object) {
- Object.keys(obj).forEach(key =>
- console.log(key, '=', obj[key]));
- }
-
- logProps({foo: 1, bar: 2}); // good
- logProps(7); // error
The second approach is to use the name of a class or constructor function, either builtin or custom. Examples include Array
, Date
, Error
, Map
, RegExp
, and Set
.
Here is an example that uses a custom Person
class and the builtin Date
class:
- // @flow
-
- class Person {
- // These are declarations of class properties
- // that are in each instance.
- name: string;
- birthday: Date;
- height: number;
- spouse: Person;
- // Class properties are optional and there isn't a way to make them required.
-
- constructor(name: string, birthday: Date, height: number): void {
- this.name = name;
- this.birthday = birthday;
- this.height = height;
- }
-
- marry(person: Person): void {
- this.spouse = person;
- person.spouse = this;
- }
- }
-
- const tami: Person = new Person('Tami', new Date(1961, 8, 9), 65);
- const mark: Person = new Person('Mark', new Date(1961, 3, 16), 74);
- tami.marry(mark);
-
- function logPerson(person: Person): void {
- const status: string = person.spouse ?
- 'married to ' + person.spouse.name : 'single';
- console.log(person.name + ' is ' + status + '.');
- }
-
- logPerson(mark); // good
- 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.
- // @flow
-
- type PersonType = {
- name: string,
- birthday: Date,
- spouse?: ?PersonType // preceding a type with ? allows null and undefined
- // The height property was purposely omitted to show that
- // objects of this type can have additional properties.
- };
-
- const tami: PersonType = {
- name: 'Tami',
- birthday: new Date(1961, 8, 9),
- height: 65
- };
-
- const mark: PersonType = {
- name: 'Mark',
- birthday: new Date(1961, 3, 16),
- height: 74,
- spouse: tami
-
- tami.spouse = mark;
-
- function logPerson(person: PersonType): void {
- const status = person.spouse ?
- 'married to ' + person.spouse.name : 'single';
- console.log(`${person.name} is ${status}.`);
- }
-
- 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.
- // @flow
-
- type PlayerToNumberMapType = {
- [player: string]: number // "player" just serves as a description of the keys
- };
-
- const playerToNumberMap: PlayerToNumberMapType = {
- 'Mario Lemieux': 66,
- 'Wayne Gretzky': 99
- };
-
- Object.keys(playerToNumberMap).forEach(player => {
- const number = playerToNumberMap[player];
- console.log(`${player} is number ${number}`);
- });
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:
- // @flow
-
- // These classes could be more involved.
- class Animal {}
- class Mineral {}
- class Vegetable {}
-
- type AMVType = Animal | Mineral | Vegetable;
-
- const alive = false;
- const grows = false;
- const clazz: Class<AMVType> = alive ? Animal : grows ? Vegetable : Mineral;
-
- const thing: AMVType = new clazz();
- 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:
- class MyClass<T> {
- // Use T in the definitions of properties and/or methods.
-
- someProp: T;
-
- constructor(someProp: T) {
- this.someProp = someProp;
- }
-
- getSomeProp(): T {
- return this.someProp;
- }
- }
-
- const myObj: MyClass<number> = new MyClass(3);
- console.log(myObj.getSomeProp()); // 3
-
- type PricedType<T> = {
- item: T, price: number, date: Date
- };
-
- function logPricedType<T>(priced: PricedType<T>) {
- console.log(String(priced.item),
- 'cost', priced.price,
- 'on', priced.date.toDateString());
- }
-
- const apple: PricedType<string> = {
- item: 'Gala apple',
- price: 0.99,
- date: new Date()
- };
- logPricedType(apple); // Gala apple cost 0.99 on Fri Apr 07 2017
-
- class Fruit {
- kind: string;
- constructor(kind: string): void {
- this.kind = kind;
- }
- toString(): string {
- return this.kind;
- }
- }
- const banana: PricedType<Fruit> = {
- item: new Fruit('Chiquita banana'),
- price: 0.29,
- date: new Date()
- };
- 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:
- // @flow
-
- type PlayerType = {
- name: string,
- number: number,
- position: string
- };
-
- const gretzky: PlayerType =
- {name: 'Wayne Gretzky', number: 99, position: 'center'};
- const lemieux: PlayerType =
- {name: 'Mario Lemieux', number: 66, position: 'center'};
-
- const players: PlayerType[] = [gretzky, lemieux];
- const playerSet: Set<PlayerType> = new Set(players);
- const playerMap: Map<number, PlayerType> = new Map();
-
- for (const player of players) {
- playerMap.set(player.number, player);
- }
-
- console.log('map-set.js: playerSet =', playerSet);
- console.log('map-set.js: playerMap =', playerMap);
The output from this code is:
- map-set.js: playerSet = Set {
- { name: 'Wayne Gretzky', number: 99, position: 'center' },
- { name: 'Mario Lemieux', number: 66, position: 'center' } }
- map-set.js: playerMap = Map {
- 99 => { name: 'Wayne Gretzky', number: 99, position: 'center' },
- 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:
- interface Vehicle {
- start(): void,
- stop(): void
- }
-
- class Boat implements Vehicle {
- start(): void { console.log('The boat is started.'); }
- stop(): void { console.log('The boat is stopped.'); }
- }
-
- class Car implements Vehicle {
- start(): void { console.log('The car is started.'); }
- stop(): void { console.log('The car is stopped.'); }
- }
-
- class House {} // has no methods
-
- function testDrive(vehicle: Vehicle) {
- vehicle.start();
- vehicle.stop();
- }
-
- const boat: Vehicle = new Boat();
- testDrive(boat); // good
-
- const car: Vehicle = new Car();
- testDrive(car); // good
-
- const house: Vehicle = new House();
- 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.
- type PrimitiveType = boolean | number | string;
- let value: PrimitiveType = true;
- value = 7; // good
- value = 'foo'; // good
- value = {}; // error
-
- // These are object types with a specific value for their "type" property.
- type AnimalType = {name: string, type: 'animal'};
- type MineralType = {name: string, type: 'mineral'};
- type VegetableType = {name: string, type: 'vegetable'};
- type ThingType = AnimalType | MineralType | VegetableType;
-
- const dog: AnimalType = {name: 'Dasher', type: 'animal'};
- const mineral: MineralType = {name: 'amethyst', type: 'mineral'};
- const vegetable: VegetableType = {name: 'corn', type: 'vegetable'};
-
- let thing: ThingType = dog; // good
- console.log(thing.name); // Dasher
- thing = mineral; // good
- console.log(thing.name); // amethyst
- thing = vegetable; // good
- console.log(thing.name); // corn
- thing = {name: 'bad', type: 'other'}; // error
Unions can be used for enums. Here is an example:
- type ActivityType = 'swim' | 'bike' | 'run';
- // In TypeScript this is called a "String Literal Type".
-
- let activity: ActivityType = 'swim'; // good
- console.log('Your current activity is', activity);
-
- activity = 'bike'; // good
- console.log('Your current activity is', activity);
-
- activity = 'run'; // good
- console.log('Your current activity is', activity);
-
- 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:
- // @flow
-
- // This type matches any object that has
- // a "name" property with a type of string.
- export type NamedType = {name: string};
-
- export function sayHello(thing: NamedType): void {
- console.log('Hello, ' + thing.name + '!');
- }
Here is the content of a file named type-alias-import.js
that imports the type alias and function:
- // @flow
-
- import type {NamedType} from './type-alias-export';
- import {sayHello} from './type-alias-export';
-
- const mark: NamedType = {name: 'Mark', hobby: 'running'};
- sayHello(mark); // good
- sayHello({name: 'Tami', hobby: 'swimming'}); // good
- 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:
- Install "dev dependencies".
- Add npm scripts to
package.json
. - Setup ESLint.
- Setup Babel.
- Create a
.flow-config
file. - Use
flow-typed
to get dependency type declarations. - 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 thesrc
directory into thebuild
directory."flow": "flow"
This runsflow
on all.js
files in the project or only those specified in.flowconfig
(described later)."floww": "flow-watch"
This is the same as theflow
script, but it keeps running, watching files for changes."lint": "eslint --quiet src"
This runseslint
on all.js
files under thesrc
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.
- "flowtype/boolean-style": ["error", "boolean"],
- "flowtype/define-flow-type": ["error", {"no-undef": "error"}],
- "flowtype/delimiter-dangle": ["error", "never"],
- "flowtype/generic-spacing": ["error", "never"],
- "flowtype/no-dupe-keys": "error",
- "flowtype/no-primitive-constructor-types": "error",
- "flowtype/no-weak-types": "warn",
- "flowtype/object-type-delimiter": ["error", "comma"],
- "flowtype/require-parameter-type": "off",
- "flowtype/require-return-type": "off",
- "flowtype/require-valid-file-annotation": "off",
- "flowtype/semi": ["error", "always"],
- "flowtype/sort-keys": "off",
- "flowtype/space-after-type-colon": ["error", "always"],
- "flowtype/space-before-generic-bracket": ["error", "never"],
- "flowtype/space-before-type-colon": ["error", "never"],
- "flowtype/type-id-match": "error",
- "flowtype/union-intersection-spacing": ["error", "always"],
- "flowtype/use-flow-type": "error",
- "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:
- {
- "presets": [
- ["env", {
- "targets": {
- "node": 7.7
- }
- }]
- ],
- "plugins": [
- "transform-flow-strip-types"
- ]
- }
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:
- Install the tool with "
npm install -g flow-typed
". - "
cd
" to the top project directory. - Enter "
flow-typed install
". This creates aflow-typed
directory if it isn't already present. It then installs type declarations there for all dependencies found inpackage.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. - 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:
- Atom - See flow-ide, Nuclide (from Facebook), and linter-flow. Note that the package "flow" is for the haxe flow build tool, not Facebook Flow.
- emacs - See https://github.com/flowtype/flow-for-emacs and https://github.com/lbolla/emacs-flycheck-flow.
- Sublime - See https://github.com/SublimeLinter/SublimeLinter-flow.
- Visual Studio Code - See Flow Language Support and vscode-flow-ide. Disable default syntax validation in the TypeScript section by setting "javascript.validate.enable" to false. This runs Flow on file saves. Hover over a variable or function name to see its type.
- WebStorm - See https://blog.jetbrains.com/webstorm/2016/11/using-flow-in-webstorm/.
- Vim - There are two options.
Option #1 is the Asynchronous Linting Environment (ALE) at https://github.com/w0rp/ale. This integrates with a large number of linters for many syntaxes. To enable the use of ESLint and Flow for JavaScript files, add the following to your.vimrc
file:
- let g:ale_linters = {
- \ 'javascript': ['eslint', 'flow'],
- \}
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
.
- // @flow
- import filer from './filer';
-
- filer('./haiku.txt', (lineCount: number) => {
- console.log('line count is', lineCount);
- });
The file src/filer.js
uses the Liner
class from the liner
npm package and the startCase
function from the lodash
package.
- // @flow
- const Liner = require('liner');
- const _ = require('lodash/string');
-
- /**
- * Outputs each line in the text file at the given path,
- * capitalizing the first letter of each word,
- * and calls cb with the number of lines read.
- */
- function processFile(path: string, cb: (number) => void): void {
- let count = 0;
- const liner = new Liner(path);
-
- liner.on('readable', () => {
- while (true) {
- const line = liner.read();
- if (line === null) break;
- console.log(_.startCase(line));
- count++;
- }
- });
-
- liner.on('end', () => cb(count));
-
- liner.on('error', err => console.error(err));
- }
-
- export default processFile;
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:
- {
- "name": "sample-project",
- "version": "1.0.0",
- "dependencies": {
- "liner": "^0.3.3",
- "lodash": "^4.17.3"
- },
- "devDependencies": {
- "babel-cli": "^6.24.0",
- "babel-eslint": "^7.2.1",
- "babel-plugin-transform-flow-strip-types": "^6.22.0",
- "babel-preset-env": "^1.2.2",
- "eslint": "^3.18.0",
- "eslint-plugin-flowtype": "^2.30.4",
- "flow-bin": "^0.42.0",
- "flow-watch": "^1.1.1",
- "npm-run-all": "^4.0.2"
- },
- "scripts": {
- "babel": "babel src -d build",
- "flow": "flow",
- "floww": "flow-watch",
- "lint": "eslint --quiet src",
- "run": "node build/index.js",
- "start": "npm-run-all lint flow babel run"
- }
- }
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 methodsElement
class, whose instances are typically created using JSXPropTypes
object type which includes properties for declaring the types of component props in the React way includingany
,array,
arrayOf,
bool
,element,
func,
instanceof
,node
,number
,object
,objectOf
,oneOf
,oneOfType
,shape
, andstring
react
module which includescreateElement
,renderToString
, and morereact-dom
module which includesfindDOMNode
and moreSyntheticEvent
and subclasses includingSyntheticDragEvent
,SyntheticInputEvent
,SyntheticKeyboardEvent
,SyntheticMouseEvent
,SyntheticTouchEvent
,SyntheticWheelEvent
, 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.
- In source files. This is the preferred location when source files can be modified.
- In type declaration files in the
flow-typed
directory of the project. This is the preferred location when source files cannot be modified. - In type declaration files a directory specified in the "
include
" section of the.flowconfig
file. Typically theflow-typed
directory should be used instead. - In a
.js.flow
file in the same directory as the source file whose types it defines. For example, types for the filefoo.js
can be specified in a file namedfoo.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:
- function double(n) {
- return n * 2;
- }
-
- exports.double = double;
Here is an example of code that uses this module:
- // @flow
- const math = require('./math');
-
- console.log(math.double(3)); // good
- console.log(math.double('bad')); // error
Here is a Flow declaration file for this CommonJS module:
- declare module './math' {
- # type declarations go here
- declare module.exports: {
- double(n: number): number;
- }
- }
Here is an example of the same simple module defined using ES module syntax:
- export function double(n) {
- return n * 2;
- }
Here is an example of code that uses this module:
- // @flow
- import * as math from './math';
-
- console.log(math.double(3)); // good
- console.log(math.double('bad')); // error
Here is a Flow declaration file for this ES module:
- declare module './math' {
- # type declarations go here
- declare export function double(n: number): number;
- }
.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
- Main Flow site: https://flow.org/
- Flow type cheat sheet: http://www.saltycrane.com/blog/2016/06/flow-type-cheat-sheet/
Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.