Flow

a bite of soundness

A type-system is sound implies that all of type-checked programs are correct (in the other words, all of the incorrect program can't be type checked), i.e. there won't be any false negative.

A type-system is complete implies that all of the correct program can be accepted by the type checker, i.s. there won't be any false positive.

Soundness and completeness

Soundness and completeness

Flow tries to be as sound and complete as possible. But because JavaScript was not designed around a type system, Flow sometimes has to make a tradeoff. When this happens Flow tends to favor soundness over completeness, ensuring that code doesn’t have any bugs.

 

Soundness is fine as long as Flow isn’t being too noisy and preventing you from being productive. Sometimes when soundness would get in your way too much, Flow will favor completeness instead. There’s only a handful of cases where Flow does this.



function square(n) {
  return n * n;
}

square('2');

Example 1

Type inference.

// @flow

function square(n) {
  return n * n;
}

square('2');

Example 1

Type inference.

// @flow

function square(n) {
  return n * n; // string. The operand of an arithmetic operation must be a number.
}

square('2');

Example 1

Type inference.

// @flow

function square(n: number): number {
  return n * n;
}

square('2');

Example 1

Type inference.

// @flow

function square(n: number): number {
  return n * n;
}

square('2'); // string. This type is incompatible with the expected param type of number.

Example 1

Type inference.

// @flow

function foo(x): string {
  if (typeof x === 'number') {
    return x;
  }
  return 'default string';
}

Example 2

Type refinement.

// @flow

function foo(x): string {
  if (typeof x === 'number') {
    return x; // number. This type is incompatible with the expected return type of string.
  }
  return 'default string';
}

Example 2

Type refinement.

// @flow

function foo(num: number) {
    if (num > 5) {
      return 'cool';
    }
}

const x = foo(9).toString();
const y = foo(1).toString();

Example 3

Nullable & non-nullable types.

// @flow

function foo(num: number) {
    if (num > 5) {
      return 'cool';
    }
}

const x = foo(9).toString();
const y = foo(1).toString(); // call of method `toString`.
                             // Method cannot be called on possibly null value.

Example 3

Nullable & non-nullable types.

// @flow

function foo(num: number): string { // string.
    if (num > 5) {                  // This type is incompatible with an
      return 'cool';                // implicitly-returned undefined.
    }
}

const x = foo(9).toString();
const y = foo(1).toString();

Example 3

Nullable & non-nullable types.

// @flow

type TypeA = 1 | 2;
type TypeB = 1 | 2 | 3;

let x: TypeA = ...;
let y: TypeB = ...;

Example 4

Subset types.

// @flow

type TypeA = 1 | 2;
type TypeB = 1 | 2 | 3;

let x: TypeA = ...;
let y: TypeB = ...;

x = y;
y = x;

Example 4

Subset types.

// @flow

type TypeA = 1 | 2;
type TypeB = 1 | 2 | 3;

let x: TypeA = ...;
let y: TypeB = ...;

x = y; // number literal `3`. This type is incompatible with number enum.
y = x;

Example 4

Subset types.

// @flow

type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB;

Example 5

Subtypes.

// @flow

type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB; // Works!

Example 5

Subtypes.

// @flow

type ObjectA = { foo: string };
type ObjectB = { foo: number, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB;

Example 5

Subtypes.

// @flow

type ObjectA = { foo: string };
type ObjectB = { foo: number, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB; // ObjectB. This type is incompatible with ObjectA.

Example 5

Subtypes.

// @flow

type FuncType = (1 | 2) => "A" | "B";

let f1: (1 | 2) => "A" | "B" | "C" = (x) => /* ... */

Example 6

Function subtypes.

// @flow

type FuncType = (1 | 2) => "A" | "B";

let f1: (1 | 2) => "A" | "B" | "C" = (x) => /* ... */
let f2: (1 | null) => "A" | "B" = (x) => /* ... */

Example 6

Function subtypes.

// @flow

type FuncType = (1 | 2) => "A" | "B";

let f1: (1 | 2) => "A" | "B" | "C" = (x) => /* ... */
let f2: (1 | null) => "A" | "B" = (x) => /* ... */
let f3: (1 | 2 | 3) => "A" = (x) => /* ... */

Example 6

Function subtypes.

Example 6

Function subtypes.

In general, the function subtyping rule is this: A function type B is a subtype of a function type A if and only if B’s inputs are a superset of A’s, and B’s outputs are a subset of A’s. The subtype must accept at least the same inputs as its parent, and must return at most the same outputs.

// @flow

type FuncType = (1 | 2) => "A" | "B";

let f1: (1 | 2) => "A" | "B" | "C" = (x) => /* ... */; // no
let f2: (1 | null) => "A" | "B" = (x) => /* ... */;    // no
let f3: (1 | 2 | 3) => "A" = (x) => /* ... */;         // ok

Example 6

Function subtypes.

Example 7

Type variance.

Supertypes
Subtypes

Example 7

Type variance.

Supertypes
Subtypes
Bivariance

Example 7

Type variance.

Supertypes
Subtypes
Bivariance Covariance

Example 7

Type variance.

Supertypes
Subtypes
Bivariance Covariance
Contrvariance

Example 7

Type variance.

Supertypes
Subtypes
Bivariance Covariance
Contrvariance Invariance

Example 7

Type variance.

All of this is why Flow has contravariant inputs (accepts less specific types to be passed in), and covariant outputs (allows more specific types to be returned).

Example 8

Nominal & structural typing.

// @flow

type FuncType = (input: string) => void;
function func(input: string) { /* ... */ }
let test: FuncType = func; // Works!

Functions - structural.

Example 8

Nominal & structural typing.

// @flow

type ObjType = { property: string };
let obj = { property: "value" };
let test: ObjType = obj;

Objects - structural.

Example 8

Nominal & structural typing.

// @flow

class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }
let test: Foo = new Bar(); // Error!

Classes - nominal.

Example 8

Nominal & structural typing.

// @flow

type Interface = {
  method(value: string): void;
};

class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }

let test: Interface = new Foo(); // Okay.
let test: Interface = new Bar(); // Okay.

Object, so structural.

Example 9

Width subtyping.

// @flow

function method(obj: {  foo: string  } & {  bar: number  }) {
  if (obj.foo) {
    (obj.foo: string);
  }
}

Example 9

Width subtyping.

// @flow

function method(obj: {  foo: string  } & {  bar: number  }) {
  if (obj.foo) {
    (obj.foo: string); // property `foo` of unknown type.
  }                    // This type is incompatible with string.
}

Example 9

Width subtyping.

// @flow

function method(obj: {| foo: string |} & {| bar: number |}) {
  if (obj.foo) {
    (obj.foo: string);
  }
}

Real life 1

// @flow

       function foo(x) {
  return x * 2;
}

export function bar() {
  return foo(10);
}

Modules.

// @flow

export function foo(x) {
  return x * 2;
}

export function bar() {
  return foo(10);
}

Modules.

Real life 1

// @flow

export function foo(x) { // x missing annotation.
  return x * 2;
}

export function bar() {
  return foo(10);
}

Modules.

Real life 1

// @flow

const f = (x: mixed) => x.y; // ?
const g = (x: any)   => x.y; // ?
const h = (x: *)     => x.y; // ?

// f(1);
// g(1);
// h(1);

Mixed, any and *.

Real life 2

mixed anything but safe
any anything
* infere
// @flow

const f = (x: mixed) => x.y; // property `y`. Property cannot be accessed on mixed.
const g = (x: any)   => x.y; // ok
const h = (x: *)     => x.y; // ok

// f(1);
// g(1);
// h(1);

Mixed, any and *.

Real life 2

mixed anything but safe
any anything
* infere
// @flow

const f = (x: mixed) => x.y; // property `y`. Property cannot be accessed on mixed.
const g = (x: any)   => x.y; // ok
const h = (x: *)     => x.y; // ok

f(1); // ok
// g(1);
// h(1);

Mixed, any and *.

Real life 2

mixed anything but safe
any anything
* infere
// @flow

const f = (x: mixed) => x.y; // property `y`. Property cannot be accessed on mixed.
const g = (x: any)   => x.y; // ok
const h = (x: *)     => x.y; // ok

f(1); // ok
g(1); // ok
// h(1);

Mixed, any and *.

Real life 2

mixed anything but safe
any anything
* infere
// @flow

const f = (x: mixed) => x.y; // property `y`. Property cannot be accessed on mixed.
const g = (x: any)   => x.y; // ok
const h = (x: *)     => x.y; // property `y`. Property not found in Number.

f(1); // ok
g(1); // ok
h(1); // ok

Mixed, any and *.

Real life 2

mixed anything but safe
any anything
* infere
// @flow

declare var x: mixed;

let y: number = x;                             // ?
let z: number = typeof x === 'number' ? x : 0; // ?

Mixed vs any.

Real life 3

// @flow

declare var x: mixed;

let y: number = x;                             // mixed. This type is incompatible with number.
let z: number = typeof x === 'number' ? x : 0; // ok

Mixed vs any.

Real life 3

// @flow

declare var x: any;

let y: number = x;                             // ?
let z: number = typeof x === 'number' ? x : 0; // ?

Mixed vs any.

Real life 3

// @flow

declare var x: any;

let y: number = x;                             // ok
let z: number = typeof x === 'number' ? x : 0; // ok

Mixed vs any.

Real life 3

Flow

By Radosław Miernik

Flow

Vazco TechMeeting 2017-12-11

  • 643