Notes on Flow type checking in JavaScript
Jul 22, 2016
Here are some rough notes I made while setting up Flow and reading through the official docs. These notes probably won't be that useful to anyone but me.
Vim setup with Syntastic and eslint
This was very tricky to get working well. I wanted a solution which would still work with eslint_d
and be fast (i.e. run as a background server). After a lot of struggle, I settled on the following configuration:
Plug 'scrooloose/syntastic'
let g:syntastic_javascript_checkers = ['eslint', 'flow']
let g:syntastic_javascript_eslint_exec = 'eslint_d'
let g:syntastic_javascript_flow_exe = 'flow'
let g:syntastic_aggregate_errors = 1
I also had to restart the eslint_d
server in the directory with the .eslintrc
config file.
Notes on type checking syntax
- Primitive types:
["boolean", "number", "string", "null", "void"]
any
type to opt out of type checking- Arrays with either
number[]
orArray<string>
- Tuples for finite length arrays (because JavaScript doesn't have actual tuples)
const tuple: [string, number, boolean] = ["foo", 0, true]
- One of many possible types with
string|number|boolean
- Object:
const object: {foo: string, bar: number} = {foo: "foo", bar: 0}
- Objects can also be used as lookup tables (i.e. variable keys)
const coolRating: {[id:string]: number} = {}
Object
type likeany
but must be an object, accessing any key will return a value with theany
type- For a function that also has other properties, there is a special type:
{ (x: number): string; foo: number }
- Functions:
function foo(a: number, b: number): number { return 4 }
- Arrow functions:
(num: number): number => num * 2
- Promise type:
Promise<string>
- Async functions supported and have Promise return types
Function
type (similar toObject
type) can be called with anything and return typeany
- Defining a class also defines a type with the same name
- Classes are compatible if one extends the other
- Nominal typing means two types are compatible only if there is explicit inheritance, this is unlike structural/duck typing where compatibility is determined based on the shape of the data
- Interfaces are a way to describe similar classes without needing explicit inheritance
interface Fooable { foo(): string; }
Class<SomeClass>
for the type ofSomeClass
(for if you need to assign a class to a variable or something)- Can define type aliases for complex types reused in many places
type MyType = { foo: string, bar: number, baz: (foo: string) => boolean; }
- Type aliases can accept another type to become generic types:
type GenericObject<T> = { foo: T };
- Type parameters for classes specified after the class name and for functions after the function name in angled brackets
- Arrow functions accept type parameters before the first brackets:
const flip = <A,B>([a,b]: [A,B]): [B,A] => [b,a];
Built-in types
- Type casting by putting in brackets:
(myVar: string)
- Implicit type casting by adding to a string is an exception that is allowed because it is so common in JavaScript:
((100 + "%") : string);
(null: null)
and(undefined: void)
- Optional parameters and object properties with question mark:
function optFun(foo?: string) { (foo: string|void) }
andtype optType = { foo?: string }; ({foo: "bar"}: optType); ({}: optType)
(null is not allowed though!) - Defaulted parameters are optional for callers but will always have a value in the function:
function default_fun(foo: string = "default foo") { (foo: string); }; default_fun();
- Maybe types have confusingly similar syntax to optional parameters, they include null though:
function maybe_fun(foo: ?string) { (foo: string|void|null); }
- Special
mixed
type is very similar toany
except that in order to type cast, you need to perform the necessarytypeof x === "string"
style checks first - Can have literal types:
("foo": "foo")
or(true: true)
or(1: 1)
- Literal types do not work with expressions in most cases (because they could be too dynamic to calculate):
("fo"+"o": "foo") // Does not work
- However, literal types do actually work with boolean expressions:
(!true: false); (true && false: false); (true || false: true);
- Can build up enums
type Suit = | "Diamonds" | "Clubs" | "Hearts" | "Spades";
- Weirdly, type definitions can start with a
|
after the equals sign, I think so that they can be split over multiple lines without looking awkward
More notes
- Function types can also be defined elsewhere:
type TimesTwo = (value: number) => number;
- Import and export types with
import type {Foo} from "./foo"
andexport type Foo = string
- Exported classes must annotate everything (while in non-exported classes, it's okay to use more inference)
- Bounded polymorphism to keep type information but also constrain the allowed input types:
function fooGood<T: { x: number }>(obj: T): T { console.log(Math.abs(obj.x)); return obj; }
- Can have multiple type parameters:
class ReadWriteMap<K, V> { store: { [k:K]: V }; constructor() { this.store = {}; } get(k: K): ?V { return this.store[k]; } put(k: K, v: V): void { this.store[k] = v; } }
- Some complex stuff about covariance and contravariance link, covariance useful when one type parameter is read only (only appears as output), contravariance useful when one type parameter is only used as input, to use this put a plus before the type param
class ReadOnlyMap<K, +V> {}
(I think it's the same syntax for both of them) - Special
this
type in classes, can only be used in output positions, might be used in a method that clones itself so that if it is extended then it will return the actual extended type (e.g. if D extends C and C had a method to clone itself then if the clone method had return type ofthis
then calling the clone method on an instance of D will return an instance of D) - Calling with too many arguments is allowed, a trick to prevent it:
function takesOnlyOneNumber(x: number, ...rest: Array<void>) {}; takesOnlyOneNumber(1, 2) // Error
- Can overload in declarations:
declare function foo(...rest: Array<void>): string; declare function foo(a: number, ...rest: Array<void>): string; declare function foo(a: number, b: number, ...rest: Array<void>): string;
(but this is not supported outside the prelude and is generally not a great idea and you should generally use union types where possible) - To check for not null or undefined, can use
myVar == null
becauseundefined == null
butundefined !== null
- With destructuring, you can skip indices with a double comma:
var [a, b, ,c] = arr;
- Intersection type requires something to be all of the input types:
({a: 1, b: ""}: {a: number} & {b: string})
- Can assign a class to the evaluation of
typeof
, I don't see how this would be very useful, the example given uses static methods,var b: typeof X = X;
- For checking if something is an array, use
Array.isArray()
- Can import and export types, can also
import typeof {jimiguitar as GuitarT} from "./User.js";
- Exports should always have explicit type annotations (both for performance and good practice)
- Can use declarations to state that something has certain types, even if the thing does not exist yet (it will be provided globally at runtime or come from an external module for example)
declare var DEBUG: bool;
- Mixins exist for declarations but they don't look like they would be a good idea to actually use
- Declared types can be made visible throughout the project with some special config
- Can declare modules to provide typing for modules with no type information found in them
declare module "fancy-pants" { declare var DEBUG: bool; }
- Files with a
.js.flow
extension take precedence over type information in the original js file - Should pretty much just work with React