mmap.page

Dive into Flow

Peek at the type system #

Built-in types #

Boolean #

JavaScript specifies many implicit conversions, which provide boolean semantics to values of other types. Flow allows this in conditional of if statements etc.

Note that in JavaScript Boolean(0) and new Boolean(0) are different. The formal is converting 0 to the boolean primitive type via the Boolean function, the later is initialize a Boolean wrapped object, which is rarely used. Same thing applies to number/Number and string/String.

Number #

JavaScript has a single number type, which is IEEE 754 floating point numbers.

Literal types #

true, false, any number, and any string is a literal type.

let wrong: false = true && false is valid since a type checker can infer that for t typed true and f typed false, t && f is typed false, just like a type checker can infer that for m and n typed number, m + n is typed number. Unlike boolean, literal types of number and string has infinite members, a type checker cannot infer if the sum of two number typed as literal type has another specific literal type. So let two: 2 = 1 + 1 is invalid.

Declaration files #

Flow's declaration files are like C's header files.

A declaration file of foo.js is named foo.js.flow:

/* @flow */

declare export function isLeapYear(year: string): boolean;
declare export let DEBUG: boolean;

declare export class Counter {
  val: number;
  increase(): void;
}

declare export type Response = 'yes' | 'no' | 'maybe';

declare export interface Stack<T> {
  push(item: T): void;
  pop(): T;
  isEmpty(): bool;
}

Declaration files supports mixin, which can also be used in implementations, for example,

/* @flow */

declare class MyClass extends Child mixins MixinA, MixinB {}
declare class MixinA {
  a: number;
  b: number;
}
// Mixing in MixinB will NOT mix in MixinBase
declare class MixinB extends MixinBase {}
declare class MixinBase {
  c: number;
}
declare class Child extends Base {
  a: string;
  c: string;
}
declare class Base {
  b: string;
}

var c = new MyClass();
(c.a: number); // Both Child and MixinA provide `a`, so MixinA wins
(c.b: number); // The same principle holds for `b`, which Child inherits
(c.c: string); // mixins does not copy inherited properties,
               // so `c` comes from Child

Problems #

Interfaces use structural typing. #

Go's FAQ suggests to add an additional method to doc implementations:

type Fooer interface {
    Foo()
    ImplementsFooer()
}

This is a simple workaround for documentation and it prevents accidental implementation of interfaces. However, it does not prevent a later modification of implementation of Fooer to remove Foo() and make ImplementsFooer() not correct any more.

With JavaScript, a much better workaround is possible (mentioned in #2376):

/* @flow */
interface A {
  foo(): number;
}

class AImpl {
  constructor(): A {
    return this;
  }
  foo(): number {
    return 1
  }
}

So flow's structural typed interface is not as hopeless bad as Go.

Implicitly type casting number to string with +. #

The number + string idiom is fairly common, so flow accepts it, like TypeScript.

With this patch, flow will reject number + string:

Error: main.js:3
  3:    return 1 + "s"
               ^ number. This type cannot be added to
  3:    return 1 + "s"
                   ^^^ string

Hegel #

Hegel has a more sound type system than Flow, and it does not have the two problems mentioned above:

  1. It dose not support interface.
  2. No type coercion for number + string.

It borrows Flow's syntax for type annotation, thus JavaScript can be generated via Flow tools (@babel/preset-flow or flow-remove-types). However, it uses TypeScript d.ts files for library definition, because TypeScript has a much larger ecosystem than Flow. Note some types are not supported, for example, Flow's interface and TypeScript's conditional typing.

Its vscode extension is still in development.