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

http://stateofjs.com/2016/flavors/

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#

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

3rd Party Libraries in Flow

IDE Support

Visual Studio Code

Atom / Nuclide

IntelliJ IDEA / Webstorm

Starting from 2016.3

Part V: Epilogue

Why did Facebook build Flow?

https://twitter.com/ReactEurope/status/763337791489146880

Should I use a type checker?

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)

Wrap-Up

  • TypeScript and Flow have influenced each other heavily
  • Basic typings are pretty similar
  • Both also support React
  • Many more constructs like union, intersection, and array types in both
  • TypeScript is a compiler, Flow is a checker
  • Flow shoots for soundness, TypeScript for tool support
  • Flow has non-nullable types as defaults
  • Generics in TypeScript are easier, but less expressive
  • Flow's type system is generally more expressive
  • Flow is written in OCaml, Typescript in Typescript
  • Integration with existing 3rd party libraries a PITA in TypeScript 1.x
  • Vastly improved in TypeScript 2.x

Resources

Thank you!

Questions / Discussion

Oliver Zeigermann / @DJCordhose

http://djcordhose.github.io/flow-vs-typescript/flow-typescript-2.html