Type Systems for JavaScript Flow and TypeScript http://djcordhose.github.io/flow-vs-typescript/flow-typescript-2.html Oliver Zeigermann / @DJCordhose More extensive version including Elm: http://bit.ly/js-types

Part I: Introduction

Why using type systems? type systems make code easier to maintain type annotations can make code more readable

can make code easier to analyse

can allow for reliable refactoring

can allow for generally better IDE support

can catch some (type related) errors early Anders Hejlsberg@Build2016: Big JavaScript codebases tend to become "read-only".

http://stateofjs.com Recently published survey on the state of JavaScript

TypeScript ease of use and tool support over soundness http://www.typescriptlang.org/

By Microsoft (Anders Hejlsberg)

Based on ES6 (probably ES7/ES8)

Adds optional type annotations, visibility, and decorators

Compiler checks and removes annotations



supporting people from Java and C# land

Flow soundness, no runtime exceptions as goal http://flowtype.org/

By Facebook

Flow is a static type checker, designed to quickly find errors in JavaScript applications

Not a compiler, but checker

If present, type annotations can very easily be removed by babel for runtime



proving types for idiomatic JavaScript

Part II: Comparison

Basics

TypeScript let obj: string; obj = 'yo'; // Error: Type 'number' is not assignable to type 'string'. obj = 10; // types can be inferred (return type) function sayIt(what: string) { return `Saying: ${what}`; } const said: string = sayIt(obj); class Sayer { // mandatory what: string; constructor(what: string) { this.what = what; } // return type if you want to sayIt(): string { return `Saying: ${this.what}`; } }

Flow let obj: string; obj = 'yo'; // Error: number: This type is incompatible with string obj = 10; function sayIt(what: string) { return `Saying: ${what}`; } const said: string = sayIt(obj); class Sayer { what: string; constructor(what: string) { this.what = what; } sayIt(): string { return `Saying: ${this.what}`; } }

Right, pretty much the same Those basic features help with documentation, refactoring, and IDE support

Non-Nullable Types Talking about correctness

TypeScript function foo(num: number) { if (num > 10) { return 'cool'; } } // cool const result: string = foo(100); console.log(result.toString()); // still cool? console.log(foo(1).toString()); // error at runtime "Cannot read property 'toString' of undefined" TypeScript does not catch this

Flow function foo(num: number) { if (num > 10) { return 'cool'; } } // error: call of method `toString`. // Method cannot be called on possibly null value console.log(foo(100).toString()); Flow does catch this But why?

Flow does not infer string as the return type The inferred type is something else // error: return undefined. This type is incompatible with string function foo(num: number): string { if (num > 10) { return 'cool'; } } // nullable type: the one inferred function foo(num: number): string | void { if (num > 10) { return 'cool'; } } // to fix this, we need to check the result const fooed: string|void = foo(100); if (fooed) { fooed.toString(); }

Enter TypeScript 2.x function foo(num: number) { if (num > 10) { return 'cool'; } } // to fix this, we need to check the result const fooed: string|void = foo(100); if (fooed) { fooed.toString(); } // or tell the compiler we know better (in this case we actually do) fooed!.toString(); Important: Only works when strictNullChecks option is checked More Improvements in TypeScript 2.0 Catches up with Flow on control flow analysis (needed to make null checks really useful)

Non-nullable types Types are nullable by default in TypeScript 1.x Non-nullable are still possible in TypeScript 1.x in a limited way Types are non-nullable by default in Flow TypeScript 2.x makes types non-nullable by default using `strictNullChecks` in `tsconfig.json`

Caveat Neither Flow nor TypeScript 2.0 catch this class Person { name: string; constructor() { // why no error? } } const olli: Person = new Person(); // issues error as expected const daniel: Person = { }; At least in TypeScript, this won't be fixed: https://github.com/Microsoft/TypeScript/issues/8476

Generics aka Parametric Types

Consider this type hierarchy class Animal { name: string; constructor(name: string) { this.name = name; } } class Dog extends Animal { // just to make this different from cat goodBoyFactor: number; } class Cat extends Animal { purrFactor: number; }

Generic Type information Types can be parameterized by others Most common with collection types let cats: Array<Cat> = []; // can only contain cats let animals: Array<Animal> = []; // can only contain animals // nope, no cat cats.push(10); // nope, no cat cats.push(new Animal('Fido')); // cool, is a cat cats.push(new Cat('Purry')); // cool, cat is a sub type of animal animals.push(new Cat('Purry'));

Up to this point this pretty much works in Flow and TypeScript the same way ...

... but wait

TypeScript let cats: Array<Cat> = []; // can only contain cats let animals: Array<Animal> = []; // can only contain animals // error TS2322: Type 'Animal[]' is not assignable to type 'Cat[]'. // Type 'Animal' is not assignable to type 'Cat'. // Property 'purrFactor' is missing in type 'Animal'. cats = animals; // wow, works, but is no longer safe animals = cats; // because those are now all cool animals.push(new Dog('Brutus')); animals.push(new Animal('Twinky')); // ouch: cats.forEach(cat => console.log(`Cat: ${cat.name}`)); // Cat: Purry // Cat: Brutus // Cat: Twinky TypeScript allows for birds and dogs to be cats here :)

Flow let cats: Array<Cat> = []; // can only contain cats let animals: Array<Animal> = []; // can only contain animals // ERROR // property `purrFactor` of Cat. Property not found in Animal cats = animals; // same ERROR animals = cats; End of story for Flow

Why? TypeScript parametric types are compatible if the type to assign from has a more special type parameter seems most intuitive allows for obviously wrong code, though

Flow using generic types you can choose from invariant (exact match), covariant (more special), and contravariant (more common) Array in Flow has an invariant parametric type more expressive harder to understand



Advanced: Bounds in Generics Both Flow and TypeScript support upper, not lower bounds Both Flow and TypeScript support F-Bounded Polymorphism https://flowtype.org/blog/2015/03/12/Bounded-Polymorphism.html https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#type-parameters-as-constraints

Part III: Are Flow and TypeScript like Java/C++/C#?

Not really Both optionally typed / any built to match common JavaScript programming patterns type systems more expressive type inference control flow based type analysis

TypeScript semantically compatible with JavaScript

Flow just a checker not even a language of its own non-nullability as default



Structural vs Nominal Typing Nominal Typing: types are compatibility when their declared types match

Structural Typing: types are compatibility when their structures match

Java, C#, C++, C all use nominal typing exclusively

Flow classes are also treated as nominal types

TypeScript classes are treated as structural types

Everything else in both Flow and TypeScript uses structural typing

Nominal Typing for Flow classes class Person { name: string; } class Dog { name: string; } let dog: Dog = new Dog(); // nope, nominal type compatibility violated let person: Person = dog; // ERROR: Dog: This type is incompatible with Person // same problem let person: Person = { // ERROR: object literal: This type is incompatible with Person name: "Olli" };

Structural Typing for TypeScript classes class Person { name: string; } class Dog { name: string; } let dog: Dog = new Dog(); // yes, correct, as structurally compatible let person: Person = dog; // same thing, also correct let person: Person = { name: "Olli" };

Structural Typing for both TypeScript and Flow // this is fine as nominal typing only applies to Flow classes let namedObject: NamedObject = dog; // same thing, also fine let namedObject: NamedObject = { name: "Olli" }; // not fine in either let namedObject: NamedObject = { firstName: "Olli" }; // cool in flow, but TypeScript wants perfect match with object literal // ERROR: Object literal may only specify known properties, // and 'firstName' does not exist in type 'NamedObject'. let namedObject: NamedObject = { name: "Olli", firstName: "Olli" };

Classes in TypeScript TypeScript has special support for classes Similar features can be found in Java/C++/C# abstract classes and methods

special shortcut constructors (combined definition of fields and initialization)

interfaces

public, private, protected

decorators (aka Attributes in C# and Annotations in Java)

readonly properties (TypeScript 2) like in C# Flow does not feature those or any other syntactic sugar, as it is a checker only

Part IV: Integrations

Integrations of raw JavaScript files

TypeScript Declaration files Core Declarations come with TypeScript compiler

Needs External Type Declarations for 3rd party libraries

Managed by Typings tools (typings install dt~mocha --save)

Will be made obsolete by npm (npm install @types/react --save)

If there are no existing declaration files Bad luck Use allowJs option to include raw JavaScript ( introduced in 1.8) Write a dummy module declaration (vastly simplified in 2.0)



3rd Party Libraries in Flow Core Declarations come with Flow Checker, includes React

Other external declarations are optional

External Flow Type Definitions

by far less libraries covered than TypeScript

some libraries even come with added flow type declarations (e.g. immutable.js)

Declarations can be added to flow config files

If there are no existing declaration files: still works, but less powerful

IDE Support

Visual Studio Code https://code.visualstudio.com/

Excellent TypeScript support

Directly uses Language Service of TypeScript Compiler

Written in TypeScript

Atom / Nuclide https://atom.io/

https://nuclide.io/ (Atom Package)

Probably best Flow support

Part V: Epilogue

Why did Facebook build Flow? https://twitter.com/ReactEurope/status/763337791489146880

Should I use a type checker? don't be fooled: checkers do not make your programs error free

there seems to be little or no impact on productivity

initial effort to introduce a checker is low, though (especially true for flow)

but a type system is a complex thing, it comes at a cost My recommendation if your project does not live for long: no

if your project is really simple: no

if there is a chance you will need to refactor the thing: yes

if your system is very important or even crucial for the success of your company: yes

if people enter or leave your team frequently: yes

you have substantial amount of algorithmic code: yes

Which type checker should you use? It depends on your requirements Just be sure to be well informed Don't let your decision be based on mere opinions or prejudice

One example of an informed descision https://twitter.com/DJCordhose/status/790196682097102848

One example of an informed doubt https://twitter.com/alexeygolev/status/790208311769661440

Types do not help you to avoid errors in algorithms? If so, what might be the problem here? List(tests).sort((t1, t2) => t1.id - t2.id) Those ids actually are strings, result always is NaN // this would have saved you type Test = { id: string; // ... }; // error 'string' This type is incompatible with 'number' List(tests).sort((t1, t2) => t1.id - t2.id)