Radosław Miernik
Open source? Embrace, understand, develop.
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.
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');
// @flow
function square(n) {
return n * n;
}
square('2');
// @flow
function square(n) {
return n * n; // string. The operand of an arithmetic operation must be a number.
}
square('2');
// @flow
function square(n: number): number {
return n * n;
}
square('2');
// @flow
function square(n: number): number {
return n * n;
}
square('2'); // string. This type is incompatible with the expected param type of number.
// @flow
function foo(x): string {
if (typeof x === 'number') {
return x;
}
return 'default string';
}
// @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';
}
// @flow
function foo(num: number) {
if (num > 5) {
return 'cool';
}
}
const x = foo(9).toString();
const y = foo(1).toString();
// @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.
// @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();
// @flow
type TypeA = 1 | 2;
type TypeB = 1 | 2 | 3;
let x: TypeA = ...;
let y: TypeB = ...;
// @flow
type TypeA = 1 | 2;
type TypeB = 1 | 2 | 3;
let x: TypeA = ...;
let y: TypeB = ...;
x = y;
y = x;
// @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;
// @flow
type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };
let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB;
// @flow
type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };
let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB; // Works!
// @flow
type ObjectA = { foo: string };
type ObjectB = { foo: number, bar: number };
let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB;
// @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.
// @flow
type FuncType = (1 | 2) => "A" | "B";
let f1: (1 | 2) => "A" | "B" | "C" = (x) => /* ... */
// @flow
type FuncType = (1 | 2) => "A" | "B";
let f1: (1 | 2) => "A" | "B" | "C" = (x) => /* ... */
let f2: (1 | null) => "A" | "B" = (x) => /* ... */
// @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) => /* ... */
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
Supertypes | ✓ | ✗ |
---|---|---|
Subtypes | ||
✓ | ||
✗ |
Supertypes | ✓ | ✗ |
---|---|---|
Subtypes | ||
✓ | Bivariance | |
✗ |
Supertypes | ✓ | ✗ |
---|---|---|
Subtypes | ||
✓ | Bivariance | Covariance |
✗ |
Supertypes | ✓ | ✗ |
---|---|---|
Subtypes | ||
✓ | Bivariance | Covariance |
✗ | Contrvariance |
Supertypes | ✓ | ✗ |
---|---|---|
Subtypes | ||
✓ | Bivariance | Covariance |
✗ | Contrvariance | Invariance |
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).
// @flow
type FuncType = (input: string) => void;
function func(input: string) { /* ... */ }
let test: FuncType = func; // Works!
// @flow
type ObjType = { property: string };
let obj = { property: "value" };
let test: ObjType = obj;
// @flow
class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }
let test: Foo = new Bar(); // Error!
// @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.
// @flow
function method(obj: { foo: string } & { bar: number }) {
if (obj.foo) {
(obj.foo: string);
}
}
// @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.
}
// @flow
function method(obj: {| foo: string |} & {| bar: number |}) {
if (obj.foo) {
(obj.foo: string);
}
}
// @flow
function foo(x) {
return x * 2;
}
export function bar() {
return foo(10);
}
// @flow
export function foo(x) {
return x * 2;
}
export function bar() {
return foo(10);
}
// @flow
export function foo(x) { // x missing annotation.
return x * 2;
}
export function bar() {
return foo(10);
}
// @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 | 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 | 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 | 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 | 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 | anything but safe |
any | anything |
* | infere |
// @flow
declare var x: mixed;
let y: number = x; // ?
let z: number = typeof x === 'number' ? x : 0; // ?
// @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
// @flow
declare var x: any;
let y: number = x; // ?
let z: number = typeof x === 'number' ? x : 0; // ?
// @flow
declare var x: any;
let y: number = x; // ok
let z: number = typeof x === 'number' ? x : 0; // ok
By Radosław Miernik
Vazco TechMeeting 2017-12-11