Flow vs TypeScript

Type Systems for JavaScript

Oliver Zeigermann / @DJCordhose

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

Most recent version including TypeScript 2.0 can be found here (might be work in progress): http://djcordhose.github.io/flow-vs-typescript/flow-typescript-2.html

Part I: Introduction

Why using type systems?

IMHO 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 errors early

Most important single use case for me

JSON style data structures

  • REST payload
  • config files
  • objects from and to database

as a close second place: find usages

TypeScript

ease of use and tool support over soundness

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
  • Works without any type annotations
  • Very good at inferring types
  • If present, type annotations can very easily be removed by babel for runtime

Part II: Comparison

aka what I find most important

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 {
	if (num > 10) {
		return 'cool';
	}
}


// to fix this, we need to check the result
const fooed: ?string = foo(100);
if (fooed) {
    fooed.toString();
}

Non-nullable types

Types are nullable in TypeScript

Types are non-nullable by default in Flow

Nullable types in Flow also possible

Non-nullable types not yet present in TypeScript
( but there is hope )

Generics

aka Parametric Types

might be a bit scary...

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

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

Part III: Epilogue

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

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
  • Flow can even understand TypeScript declaration files
  • 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 written in OCaml, Typescript in Typescript

Thank you!

Questions / Discussion

Oliver Zeigermann / @DJCordhose