Type Systems Shenanigans

Please note: not every example here is a bug. Sometimes it's just unexpected behavior resulting from the type-system design or from type system theory in general.

Generic

None of the typing systems can handle this correctly, all show no error during static analysis (but could be runtime error).

Flow (pr):

let a = [1, 2, 3];
let b: number = a[10]; // undefined
let c = b * 2;

Typescript (issue):

let a = [1, 2, 3];
let b: number = a[10]; // undefined
let c = b * 2;

ReasonML:

let a: array(int) = [|1, 2, 3|];
let b: int = a[10] // undefined
let c = b * 2

Flow

boolean is incompatible with true | false

declare function foo(true | false): void;
declare function bar(): boolean;
foo(bar());
4: foo(bar())
^ Cannot call `foo` with `bar()` bound to the first parameter because: Either boolean [1] is incompatible with boolean literal `true` [2]. Or boolean [1] is incompatible with boolean literal `false` [3].
References:
2: declare function bar(): boolean ^ [1]
1: declare function foo(true | false): void
^ [2]
1: declare function foo(true | false): void
^ [3]

https://github.com/facebook/flow/issues/4196

mixed type cannot be exhausted

declare var flowDebugPrint: $Flow$DebugPrint;
function test(x: mixed) {
if (typeof x === 'string') {
return true;
}
flowDebugPrint(x); // still 'mixed', see the output πŸ€”
}

flow.org/try

Debug output:

{
"reason": {
"pos": {
"source": "-",
"type": "SourceFile",
"start": { "line": 7, "column": 18 },
"end": { "line": 7, "column": 18 }
},
"desc": "mixed"
},
"kind": "MixedT"
}

One solution is to manually define your custom mixed type which can be exhausted.

Incorrect array destructing

const [w, a, t] = { p: '' }; // no error

flow.org/try

On the other hand, TS is OK with this code (while Flow throws an error correctly):

const foo: {} = '';

https://www.typescriptlang.org/play/index.html#code/MYewdgzgLgBAZiEAuGBvAvjAvDA5LgbiA

Defaults for non-existent properties are allowed

const React = require('react');
function Component({defaultProp = "string"}) {
return null;
}
<Component />;

This is allowed even though some people would expect something like "Error, defaultProp is missing in props", it's a feature: https://github.com/facebook/flow/commit/6dec7d5dbbd12a6f210f7c3ae21841a932eb71a8 (from 0.109.0)

Spreads don't preserve read-only-ness

type A = {| +readOnlyKey: string |}
type B = {| ...A, +otherKey: string |}
function test(x: B) {
x.readOnlyKey = 'overwrite'; // no error ?
x.otherKey = 'overwrite'; // no error ??
}

This applies to value spreads as well since they are creating a new object. It's less understandable for these type spreads where value spread is not involved.

Typescript shenanigans

Exact types only on declaration

Typescript types are exact by default but only on declaration. This means it won't catch cases like this:

type User = {
username: string;
email: string;
};
const user1: User = {
...{ username: 'x' },
...{ email: 'y' },
...{ xxx: 'y' },
yyy: 'y', // <<< only this fails
};
const user2: User = {
...{ username: 'x' },
...{ email: 'y' },
...{ xxx: 'y' },
};

Flow doesn't have exact types by default (yet) but it can handle these cases better:

type User = {|
username: string,
email: string,
|};
const user1: User = {
...{ username: 'x' },
...{ email: 'y' },
...{ xxx: 'y' }, // <<< this fails
yyy: 'y', // <<< this fails
};
const user2: User = {
...{ username: 'x' },
...{ email: 'y' },
...{ xxx: 'y' }, // <<< this fails
};
Error β”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆ src/test.js:8:21
Cannot assign object literal to user1 because:
β€’ property xxx is missing in User [1] but exists in object literal [2].
β€’ property yyy is missing in User [1] but exists in object literal [2].
5β”‚ email: string,
6β”‚ |};
7β”‚
[1][2] 8β”‚ const user1: User = {
9β”‚ ...{ username: 'x' },
10β”‚ ...{ email: 'y' },
11β”‚ ...{ xxx: 'y' },
12β”‚ yyy: 'y', // <<< only this fails
13β”‚ };
14β”‚
15β”‚ const user2: User = {
16β”‚ ...{ username: 'x' },
Error β”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆ src/test.js:15:21
Cannot assign object literal to user2 because property xxx is missing in User [1] but exists in object literal [2].
12β”‚ yyy: 'y', // <<< only this fails
13β”‚ };
14β”‚
[1][2] 15β”‚ const user2: User = {
16β”‚ ...{ username: 'x' },
17β”‚ ...{ email: 'y' },
18β”‚ ...{ xxx: 'y' },
19β”‚ };
20β”‚
Found 3 errors

Incorrect constructor instance

JS operator new doesn't guarantee the instance type so obviously. This probably results from structural TS typechecking:

class Logger {
constructor() {
return new Error('test');
}
}
const logger = new Logger();
console.warn(logger instanceof Logger); // false
console.warn(logger instanceof Error); // true

Logger is in this case instance of Error. Therefore, you can access logger.message. But TS doesn't understand this very well and incorrectly assumes that new Logger is always instance of Logger. But, you don't have guarantee that new Logger will return Logger instance without spinning full-blown typechecking. This is why Flow requires the explicit annotation in types-first architecture like so:

export default new Logger();
// after 'flow autofix exports':
export default (new Logger(): Logger);

Incorrect JSON.stringify output type

const x = JSON.stringify(undefined)
x.toLowerCase();

TS incorrectly assumes this code is safe. It would, however, result in TypeError:

> const x = JSON.stringify(undefined);
undefined
> x.toLowerCase();
Thrown:
TypeError: Cannot read property 'toLowerCase' of undefined

It's because JSON.stringify returns undefiend in this case.