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.


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;


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


boolean is incompatible with true | false

declare function foo(true | false): void;
declare function bar(): boolean;
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].
2: declare function bar(): boolean ^ [1]
1: declare function foo(true | false): void
^ [2]
1: declare function foo(true | false): void
^ [3]


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 πŸ€”


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


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

const foo: {} = '';


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β”‚ |};
[1][2] 8β”‚ const user1: User = {
9β”‚ ...{ username: 'x' },
10β”‚ ...{ email: 'y' },
11β”‚ ...{ xxx: 'y' },
12β”‚ yyy: 'y', // <<< only this fails
13β”‚ };
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β”‚ };
[1][2] 15β”‚ const user2: User = {
16β”‚ ...{ username: 'x' },
17β”‚ ...{ email: 'y' },
18β”‚ ...{ xxx: 'y' },
19β”‚ };
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)

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

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

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

TS doesn't make a difference between arrays and objects

Patterns like this one are fine from TS perspective:

interface Foo {
[id: number]: string;
const a: Foo = ['aaa', 'bbb', 'ccc'];

However, it doesn't have to be what developers expect (even though it's correct from the JS perspective). Other type systems follow the difference between these types which gives you more confidence since you cannot return array where you'd expect an object and work with it later (which will most likely break since the types are quite different).

So, how do you annotate "object with numerical indexer, not an array"? :thinking:

Accidental global access

It's very easy to access and use global variables like length or name (simple refactoring mistake) and there is not way how to prevent this, see: https://github.com/microsoft/TypeScript/issues/14306

This is a common mistake even in large companies:

We have also been bitten by the 'name' thing at Google, and have also been considering patching it out of our lib.d.ts. I think the fix that behaves how TypeScript does is to split the standard library up further so that a project can opt in or out from the global declarations.