This section briefly describes types in the context of JavaScript and through the lense of Flow.
The JavaScript language is made up of data structures like
Boolean
, Null
, Undefined
,
String
, Object
, etc... . When creating a
named data structure (e.g. a variable, a function parameter, an
object property) in JavaScript the language does not require that
you define/annotate what type(s) of data structure will be
reference/stored. In JavaScript you can create a parameter,
variable, or object property and assign it any data structure you
want and then re-assign it with a new data structure type in the
future without an error resulting from the change in data structure.
let myVariable = 1;
// myVariable now references a value that is a number type data structure
// I did not tell JavaScript that myVariable would hold a number
console.log(typeof myVariable) // logs "number"
// JS dynamically, at runtime, figured out that the structure was a number and of the type number
myVariable = 'string';
// now, myVariable references a value that is a string type data structure
console.log(typeof myVariable) // logs "string"
myVariable = {};
// now, myVariable references a value that is an object type data structure
console.log(typeof myVariable) // logs "Object"
// The JavaScript runtime figures out data structure types dynamically the developer does
// not tell the program what types before running the program
Keep In Mind:
"foo".toFixed(); // throws error
.
Because JavaScript allows type changes at runtime (e.g., when running in a browser) it is considered a dynamically typed language.
Note:
'3' * '2' === 6 // no error due to coercion
). As
languages do less and less conversions/coercion they become more
strongly typed but yet can still be dynamically typed languages
(e.g. Ruby, Python, Lua, PHP etc...)
Some developers believe that statically typed languages that have a compiling/transpiler step, which do type checking before runtime execution, are superior to dynamically typed languages that have no compiling/transpiler step. The reason being that type errors can be surfaced before running the code.
One should think of Flow as a tool that forces invariants by analysis (i.e. invariants means the program fails unless all values have a type, inferred or explicitly defined, and that that type does not change and is used correctly at all times). Flow layers itself over the top of JavaScript introducing a static analysis step where it attempts to treat JavaScript as a static programming language. Flow analyzes static code, takes hints from Flow annotations, and tells you when your code does not look like a static programming language.
Keep In Mind:
To initially grok Flow and static types in the context of JavaScript
consider that by using Flow one can define/annotate (e.g.
:string
and :number
) what type of value a
variable or parameter can store/reference. Provide the wrong type of
data in the wrong place and Flow will complain/error.
// @flow
let score:number;
score = '3'; // Error, the value must be a number not a string containing a number
function square(n: number) {
return n * n;
}
square("2"); // Error, n can't be a string, just a number!
/* Flow command line tool or Flow running in your IDE will indicate the following errors:
3: score = '3'; // Error, the value must be a number
^ Cannot assign `'3'` to `score` because string [1] is incompatible with number [2].
References:
3: score = '3'; // Error, the value must be a number
^ [1]
1: let score:number;
^ [2]
9: square("2"); // Error, n can't be a string, just a number!
^ Cannot call `square` with `"2"` bound to `n` because string [1] is incompatible with number [2].
References:
9: square("2"); // Error, n can't be a string, just a number!
^ [1]
5: function square(n: number) {
^ [2]
*/
Note that most statically type languages and static type systems like Flow will also infer types from expressions without annotations.
// @flow
/* Flow is inferring that n in the function below should be a number not a string
because it is pulling JavaScript from a weakly typed language to a strongly typed
language trying to get it to error when ambiguity is introduced because of weakness
in types or native coercion routines/rules. */
function square(n) {
return n * n; // type error inferred because expression expects numbers not strings
// Non-Flow JS would not error here, you'd get 4
// because native JavaScript try and do the correct thing
}
square("2");
/* Flow command line tool or Flow running in your IDE will indicate the following errors:
4: return n * n; // type error inferred because expression expects numbers not strings
^ Cannot perform arithmetic operation because string [1] is not a number.
References:
7: square("2");
^ [1]
4: return n * n; // type error inferred because expression expects numbers not strings
^ Cannot perform arithmetic operation because string [1] is not a number.
References:
7: square("2");
^ [1]
*/
Additionally, a type checker like Flow is not only concerned with implicit type annotations for variables, parameters, and object properties but also values returned from functions.
// @flow
function square(n: number):string {
return n * n; // Error, this returns a number not a string
}
square(2);
/* Flow command line tool or Flow running in your IDE will indicate the following errors:
4: return n * n; // Error, this returns a number not a string
^ Cannot return `n * n` because number [1] is incompatible with string [2].
References:
4: return n * n; // Error, this returns a number not a string
^ [1]
3: function square(n: number):string {
^ [2]
*/
This will briefly explain the concept of Flow and the need for it.
Flow is an open source JavaScript library written by Facebook that
statically analysis your JavaScript files as if it was a static
language instead of a dynamic langauge. It does this by inferring
types and analyzing type annotations developers write over the top
of JavaScript (e.g.
let score: number
, score can only be assigned values
that are JavaScript numbers. But this isn't valid JS so the
: number
has to be removed before a JS engine will
run).
Basically, it is a tool not unlike eslint that statically analyzes your code during development for invariant data type problems. It might be best to simply think of it as a linting tool that expects JavaScript syntax to conform to a typed system and when id does not, it errors/warns/blocks development, etc... .
No matter what anyone exposes about code quality, managing types does not mean you have to use static types and avoid dynamic types to have a maintainable code base. Flow usage is a reflection upon the subjective development whims of developers and their context (i.e. Not everyone has Facebook problems. Only facebook and their developers might have the problems that Flow fixes). It is likely the case that if you or your team requires the use of strict/strongly typed code, a bolted on type system will bring more problems than solutions. If types are an absolute requirement for your project consider using a natively type language (e.g. Reason, PureScript, Elm) before settling for Flow or TypeScript.
Keep In Mind:
With that said, many individuals and teams find Flow (i.e. bolted on type systems) extremely helpful. But this is more telling of the individual or team and the problems and tradeoffs they wish to have and not exactly based on the objective merits of Flow (i.e. bolted on type systems) itself. Flows value is in the eye of the beholder. If you are trying to figure out if Flow should be used on a team, first asked if everyone on the team agrees Flow is absolutely required given its costs (know the costs!). Be concerned about co-workers who take a neutral, naive, or negative perspective on Flow because the effort to make it valuable takes 100% commitment from everyone who writes code where Flow is used. Flow usage is the kind of choice that requires 100% participation/buy-in from all team members for its value to rise above its costs. If the team can't reach a massive majority agreement on the usage of Flow then Flow should likely be avoided because the subjective/contextual value is directly tied to the commitment level and to the quality of its implementation.
Keep in Mind:
before runtime
(i.e. earlier
errors at compile time instead of run time).
This will briefly discuss how Flow fits into most development routines.
Flow is a node command line tool that can analysis your JavaScript
files for types in near real time. Most developers have Flow
integrated into their IDE so that as they work in their code Flow is
always checking and reporting on Flow errors in real time without
having to monitor or use a command line interface (i.e. using a
.flowconfig
file).
The files that Flow checks can be designated file by file or one can configure Flow to check all files of X type in certain directories. Given that you can tell Flow exactly which files to check and which files not to check many consider Flow to be something you can gradually adopt.
To designate the files you want Flow to monitor (assuming you have installed and are running Flow), from within the files themselves, simply add the following comment to the top of the file:
// @flow
or
/* @flow */
Without adding any Flow type annotation syntax, and assuming a file
has // @flow
on the first line, Flow will infer types
for all values in your file (Including imported values).
// @flow
function square(n) {
return n * n; // type error inferred because expression expects numbers not strings
}
square("2");
/* Flow command line tool or Flow running in your IDE will indicate the following errors:
4: return n * n; // type error inferred because expression expects numbers not strings
^ Cannot perform arithmetic operation because string [1] is not a number.
References:
7: square("2");
^ [1]
4: return n * n; // type error inferred because expression expects numbers not strings
^ Cannot perform arithmetic operation because string [1] is not a number.
References:
7: square("2");
^ [1]
*/
Note that without Flow annotations (i.e. inference alone), Flow will have too:
"If Flow is unable to figure out what the exact type is for each
value, Flow must figure out what every possible value is and check
to make sure that the code around it will still work with all of
the possible types." - Flow Docs
Inference errors alone do not bring much value. The point of adding Flow to your code is so that you can annotate values with types (especially values like functions parameters, values returned from functions, Object property's, and Array values). Inferences from Flow is just a courtesy so you don't have to type everything, but Flow assumes that you are not only using Flow for inferences. Flow's value is tied up in using the annotating system it provides to explicitly track values (i.e. by using type annotations one can in a sense create logical proofs/formulas for all values so that it will be near impossible for type errors to occur at runtime.).
Flow provides a syntax, pretty much an evolution of the JavaScript language, that it unfortunately calls "Types" (This can get confusing because it does not correlate exactly to native types in JavaScript). Flow type syntax/annotations are added to your JavaScript, which is used by Flow to statically lint/analysis your code in the context of a static type system.
Flow offers the following types (i.e. really an entire type syntax, including tools and utilities, for annotating native and user defined data values in JavaScript):
Primitive Types
Literal Types
Mixed Types
Any Types
Maybe Types
Function Types
Object Types
Array Types
Tuple Types
Class Types
Type Aliases
Opaque Type Aliases
Interface Types
Generic Types
Union Types
Intersection Types
Type Casting Expressions
Utility Types
Module Types
The remainder of these notes will detail these types
As previously mentioned, Flow can infer types and can be told about types via annotations. But Flow can also restrict the language via an internal Flow linting system (i.e. restrict how you write JavaScript itself). For example it will lint/complain about passing unexpected arguments to a function.
// @flow
const myFunc = (x) => {
return x;
}
myFunc(1,2);
/* Flow command line tool or Flow running in your IDE will indicate the following errors:
5: myFunc(1,2);
^ Cannot call `myFunc` because no more than 1 argument is expected by function [1].
References:
1: const myFunc = (x) => { ^ [1]
*/
These types of errors slightly extend Flow past a type checking system and into the realm of JavaScript conventions/linting restrictions.
Any value or expression in JavaScript can be annotated. If you're new to Flow or static types this might not help you grok exactly how JavaScript is annotated with Flow types. It might initially be helpful to think about what can be annotated (or inferred) in terms of:
Variable's:
Function Parameter's:
A Function's Return Value:
Object's
Array's
Class's
Expressions
Below each of the above typing situations are simplistically and
briefly explored in order to initially reveal some insight into what
can be typed (most of the examples use very simple Flow type
annotations e.g. primitive types :string
and
:number
):
Variable's:
// @flow
// Tell Flow that this variable can only hold a
// string or undefined using primitive type annotations
let myVariable: string | void = undefined;
myVariable = 'string';
Function Parameter's:
// @flow
// Tell Flow that parameter a and b can only hold a
// string using primitive type annotations
function concat(a: string, b: string){
return a + b;
}
A Function's Return Value:
// @flow
// Tell Flow that this function can only return a
// string using primitive type annotations
function concat(a, b):string {
return a + b;
}
Object's
// @flow
// Tell Flow that myObject must be an object and must have a name property
// and the value must be a string using primitive type annotations
var myObject: {| name: string |} = { name: 'pat' };
Array's
// @flow
// Tell Flow that myArray must be an array and can only contain
// number values using the number primitive type annotation
let myArray: Array<number> = [1, 2, 3];
// the above can also be written in a shorter syntax i.e. : theType[]
let mySecondArry: number[] = [1, 2, 3];
Class's
// @flow
// Tell Flow that myClassInstance must follow the types defined in TheClass
class TheClass {
score:number = 0
noScoreMessage:string = 'No Score'
}
let myClassInstance: TheClass = new TheClass();
Expression's
// @flow
// Tell Flow that the expression 2+2 must result in a number value
(2 + 2: number);
Keep In Mind:
:any
annotation.
This section outlines basic Flow type annotation syntax. Primitive Types, Function Types, Object Types, Array Types, Class Types, Literal Types, Maybe Types, Any Type, Mixed Type.
Flow offers the following baseline annotations for the common JavaScript data type structures which I call group together and call type type annotations (i.e. these baseline types have almost a one to one relationship with native JavaScript values):
Primitive Types
Function Types
Object Types
Array Types
Class Types
Primitive Types:
A JavaScript value (e.g. variables, parameters, values returned from functions, Object properties, Array items etc...) can be annotated as a primitive value using of the following Flow type annotations:
: boolean
: number
: string
: null
: void
(Note: void is used to mean
undefined
)
// @flow
let opened : boolean = false;
let score : number = 0;
let middleName : string = 'leroy';
const constantNull : null = null;
const constantUndefined : void = undefined; // Note void is used for undefined
Function Types:
A JavaScript function value can be annotated as a function type using one of the following Flow type annotations:
: () => void
: Function
(Note:
Don't use this, be
aware however you might see its use. Instead use
any
or
(...args: Array<any>) => any)
// @flow
// myFunc has to be a function that returns a string
let myFunc : (num : number) => string;
myFunc = (num) => {
return num.toString();
};
myFunc(34241312);
Can also be written (But really ugly and hard to read):
// @flow
let myFunc : (num : number) => string = (num) => {
return num.toString();
};
myFunc(34241312);
Note:
?
character you can tell the Flow checker
that a function parameter is optional. For example,
let myFunction(myParam?:string){...}
tells Flow
that this function may or may not have a
myParam
parameter. You might wonder what is the
different between
let myFunction(myParam?:string){...}
and
let myFunction(myParam:?string){...}
where a Maybe
type is used. The difference is that a Maybe type will allow
null
while an optional function parameter will not
allow null
.
Object Types:
A JavaScript value (e.g. Object properties) can be annotated as an Object type using the following Flow type annotations (Note the properties in the Object can be typed too):
: { [key: string]: any}
or Object
: {}
// @flow
let myObj1:Object = {}; // uses : Object but this is going to be deprecated
myObj1.prop = 'prop';
// Should use : { [key: string]: any} instead of : Object
let myObj2:{[key:string]:string} = {}; // uses : { [key: string]: string}
myObj2.prop = 'prop';
Arbitrary objects (i.e. unknown properties) that you don't set properties on can be annotated like (helpful when annotating parameters):
// @flow
const arbitraryObject = {prop:'prop'};
let myObject:{} = arbitraryObject; // uses : {}
// or as parameter
function objectToString(obj: {}) {
return obj.toString()
}
objectToString(arbitraryObject);
Note:
?
character you can tell the Flow checker
that an object might have a property or it might not. For
example, let myObject : {score?:number} = {}
tells
Flow that this object may or may not have a score property with
a number value. Optional properties can either be void or
omitted altogether. However, they cannot be null
.
|
character Flow can be informed when an
object must contain a specific property(s). For example,
let myObject : {|score:number|} = {score:120}
tells Flow that this object must have a score property with a
number value. And it can't have anything less or anything more.
Without the pipes extra properties can be added to objects.
undefined
, when a property that does not exist is
accessed on an object.
Array Types:
A JavaScript value can be annotated as an Array type using the following Flow type annotations (Note that the items in the Array can be typed too):
: Array<any>
(a shorthand : any[]
)
: $ReadOnlyArray<any>
// @flow
let myArrayOne: Array<any> = [1, 'foo', true, null, {}, []];;
// a shorthand is available to the above syntax
// let myArray: any[];
let myArrayTw0: Array<number> = [1, 2, 3];;
// a shorthand is available to the above syntax
// let myArray: number[];
// possible to have Flow make sure certain Arrays are read only
const readonlyArray: $ReadOnlyArray<number> = [1, 2, 3];
Class Types:
To annotate a Class instance, where ever you create an instance of the class, annotate the instance with the class name (assuming annotations have been added to the Class definition) .
: TheClassName
// @flow
// Tell Flow that myClassInstance must follow the types defined in TheClass
class TheClass { // using https://tc39.es/proposal-class-public-fields/
score:number = 0
noScoreMessage:string = 'No Score'
}
let myClassInstance: TheClass = new TheClass();
Flow offers the Literal Type (e.g.
const noPoints:0 = 0;
) so a data structure can be
fixed to an exact primitive value or set of primitive values. The
primitive values that can be used are true
,
false
, null
, void
, any string
(e.g. 'foo'
), or any number (e.g. 1432
).
// @flow
function getColorBasedOnMessageType(name: "success" | "warning" | "danger"):string {
switch (name) {
case "success" : return "green";
case "warning" : return "yellow";
case "danger" : return "red";
}
}
/*
Notice how the | character was used to provide an
exact set of string values
/*
getColor("success");
getColor("danger");
getColor("error"); // Error, can only be "success" | "warning" | "danger"
Note that when using const
a literal value is inferred.
const foo = 'bar';
// same as const foo:bar = 'bar';
: ?[TYPE HERE]
)
Flow offers the Maybe Type (e.g.
let score: ?number
) that can be combined with any other
type to signify that the type is optional and if it does not exist
then that is ok (i.e. it is ok that null
or
undefined
was used here).
// @flow
function acceptsMaybeNumber(value: ?number) {
return value;
}
acceptsMaybeNumber(42);
acceptsMaybeNumber();
acceptsMaybeNumber(undefined);
acceptsMaybeNumber(null);
acceptsMaybeNumber("42"); // Error, can only be a number, undefined, or null
: any
)
Flow offers the Any Type (e.g. : any
)
annotation to basically opt out of using the type checker.
Essentially by using : any
you side step the checker
and allow JavaScript to do its normal thing in terms of type
coercion and error'ing.
// @flow
function add(one: any, two: any): number {
return one + two;
}
add(1, 2); // no Flow error.
add("1", "2"); // no Flow error.
add({}, []); // no Flow error.
: mixed
)
Flow offers the Unknown Type (i.e.
: mixed
) so that any data structure can be provided.
Note that if you use the mixed type then additional checks (e.g.
if
statement) have to exist that will determine its
type or Flow will complain (as opposed to :any
which
won't complain about anything)
This fails the Flow checker:
// @flow
function stringify(value: mixed) {
// $ExpectError
return "" + value; // Error!
}
stringify("foo");
/*
3: return "" + value; // Error!
^ Cannot add empty string and `value` because mixed [1] could either behave like a string or like a number.
References:
1: function stringify(value: mixed) {
^ [1]
*/
But this does not:
// @flow
function stringify(value: mixed) {
if (typeof value === 'string') {
return "" + value; // Works!
} else {
return "";
}
}
stringify("foo");
// no Flow error.
This section outlines how to organize and separate Flow types from JavaScript code
Tuples are nothing more than an Array with a fixed number of items in the Array, all having a specified Flow type.
// @flow
// Notice that a tuple has the syntax : [type, type, type]
// don't confuse this with Array types
let myTuple: [number, boolean, string] = [1, true, "three"];
let num : number = myTuple[0];
let bool : boolean = myTuple[1];
let str : string = myTuple[2];
// These error
let none = myTuple[3]; // can't access undefined items
myTuple[3] = {}; // can't set none typed items
// Tuples don't match Arrays
let array: Array<number> = [1, 2];
let tuple: [number, number] = array; // This will error
Note:
Array.prototype
methods that mutate
the tuple, only ones that do not (e.g.
let tuple: [number, number] = [1, 2].push(3);
).
Union types make it possible for a value to potentially be one of a set of different Flow types. Think of them as the ability to tell Flow, a value can be this type or this type or this type or this type.
// myValue can be a string or a number or undefined
let myValue: string | number | void;
Note:
A "disjoint union" as Flow calls it is the intersection or overlap between two types.
type emailAndName = { name: string, email: string };
type phoneAndName = { name: string, phone: string };
function checkWhichContactInfo(info: emailAndName | phoneAndName):string {
if (info.email) {
return 'email';
} else {
return 'phone';
}
}
checkWhichContactInfo({name:'bill',email:'bill@hotmail.com'});
checkWhichContactInfo({name:'jill',phone:'0-000-00000'});
Intersection types take in multiple type definitions and use all of them as a type of value.
// @flow
type A = { a: number };
type B = { b: boolean };
type C = { c: string };
function method(value: A & B & C) {
// ...
}
method({ a: 1, b: true, c: 'three' });
/* These would error
method({ a: 1 });
method({ a: 1, b: true });
*/
This section outlines how to organize and separate Flow types from JavaScript code
Flow annotations do not have to be written inline. It is possible
using the Flow type
syntax to create an aliase to Flow
annotations. This makes re-use and separation of concerns possible.
For example syntax heavy inline type definitions like this function type (i.e. inline type noise and difficult to read):
// @flow
let myFunc : (num : number) => string = (num) => {
return num.toString();
};
let myOtherFunc : (num : number) => string = (num) => {
return num.toString();
};
myFunc(34241312);
myOtherFunc(34241312);
Can be visually simplified, concerns separated, and made re-usable using typed aliases.
// @flow
// myFuncFlowAlias
type myFuncFlowAlias = (num : number) => string;
// this function takes a number and returns a string
let myFunc : myFuncFlowAlias = (num) => {
return num.toString();
};
// this function takes a number and returns a string
let myOtherFunc : myFuncFlowAlias = (num) => {
return num.toString();
};
myFunc(34241312);
myOtherFunc(34241312);
Flow Type aliases can be used anywhere a type annotation can be used.
Note:
In Flow, you can export type aliases, interfaces, and classes from one file and import them into another file.
// @flow
export default class Foo {};
export type MyObject = { /* ... */ };
export interface MyInterface { /* ... */ };
Import:
// @flow
import type Foo, {MyObject, MyInterface} from './exports';
These external resources have been used in the creation of these notes.