Typed JavaScript using TypeScript and Flow

Javaland 2017

Slides for this talk: http://bit.ly/types-javaland

Oliver Zeigermann / @DJCordhose

Extended Version (constantly updated): http://bit.ly/js-types

TL'DR Part I

  • Flow and TypeScript both supply a Type System on top of JavaScript
  • A Type System is a prerequisite for JavaScript to catch up (and even exceed) Java in terms of maintainability
  • Their Type Systems used to have significant differences
  • This, however, has become a thing of the past
  • They mainly differ in tooling and initial effort
  • TypeScript has the better tooling
  • Flow can be incrementally introduced in an existing project with ease

Why using type systems?

Anders Hejlsberg@Build2016: Big JavaScript codebases tend to become "read-only".

Introductory Demonstration

Some basic TypeScript hacking in Visual Studio Code

TypeScript (similar to what we just hacked)


// variables can have type information
let foo: string;
foo = 'yo';
// Error: Type 'number' is not assignable to type 'string'.
foo = 10;


// types can be inferred (return type)
function sayIt(what: string) {
    return `Saying: ${what}`;
}
const said: string = sayIt(obj);


class Sayer {
    what: string; // mandatory

    constructor(what: string) {
        this.what = what;
    }

    // return type if you want to
    sayIt(): string {
        return `Saying: ${this.what}`;
    }
}

Flow


// variables can have type information
let foo: string;
foo = 'yo';
// Error: number: This type is incompatible with string
foo = 10;


// types can be explicit (parameter) or inferred (return type)
function sayIt(what: string) {
    return `Saying: ${what}`;
}
const said: string = sayIt(obj);


class Sayer {
    what: string; // type also mandatory

    constructor(what: string) {
        this.what = what;
    }

    // return type if you want to
    sayIt(): string {
        return `Saying: ${this.what}`;
    }
}

Flow and TypeScript basics are pretty similar

Those basic features help with documentation, refactoring, and IDE support

Nullability

One of my main sources of runtime exceptions when programming Java

Even after many years it is still surprising how many corner cases I miss in complex code

Flow

what is the result here in pure JavaScript?


function foo(num) {
    if (num > 10) {
        return 'cool';
    }
}
console.log(foo(9).toString());

"Uncaught TypeError: Cannot read property 'toString' of undefined"

What the flow checker thinks about this


// error: call of method `toString`.
// Method cannot be called on possibly null value
console.log(foo(9).toString());

To fix this, we need to check the result


const fooed = foo(9);
if (fooed) {
    fooed.toString();
}

Types are non-nullable by default in flow

TypeScript


// both TypeScript and flow allow
// to put the type annotation here instead of using inference
function foo(num: number) {
    if (num > 10) {
        return 'cool';
    }
}

// same as flow
const fooed: string|void = foo(9);
if (fooed) {
    fooed.toString();
}

// or tell the compiler we know better (might be a bad idea)
fooed!.toString();

Only applies to TypeScript 2.x

Only works when strictNullChecks option is checked

All types nullable by default in TypeScript 1.x

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

Consider these classes


class Animal {
   name: string;
}

class Dog extends Animal {
    goodBoyFactor: number;
}

class Cat extends Animal {
    purrFactor: number;
}
           

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;
           

Flow does not have have this caveat

The flipside

This code is safe (as we access cats in a readonly fashion)


function logAnimals(animals: Array<Animal>) {
    animals.forEach(animal => console.log(`Animal: ${animal.name}`));
}

logAnimals(cats);
  • This works in TypeScript (and it should)
  • however, potentially not safe, there is nothing to keep us from writing to cats
  • Flow does not allow this, even though it is safe

much despised Java generics excel here as they can actually make that code safe (another difference: Use-site variance )


// Java
void logAnimals(List<? extends Animal> animals) {
    animals.forEach(animal -> System.out.println("Animal: " + animal.name));
    // illegal:
    animals.add(new Animal("Twinky"));
}

Differences in Generic Types

Immutable Data Structures can be Covariant in Flow


// https://facebook.github.io/immutable-js/
import {List} from 'immutable';

let animals: List<Animal> = List.of();
let dogs: List<Dog> = List.of();

// cool, as all data structures from immutable.js are read-only
animals = dogs;

class List<+T> extends IndexedCollection<T> {
  // ...
}

`any` type

can be anything, not specified

can selectively disable type checking


function func(a: any) {
    return a + 5;
}

// cool
let r1: string = func(10);

// cool
let r2: boolean = func('wat');
  • flow / TypeScript 2: explicit any supported, but any never inferred

Union Types

aka Disjoint Unions aka Tagged Unions aka Algebraic data types

to describe data with weird shapes

depending on some data other data might apply or not


 // a disjoint union type with two cases
type Response = Result | Failure;

type Result = { status: 'done', payload: Object }; // all good, we have the data
type Failure = { status: 'error', code: number}; // error, we get the error code
           

Implementation both in Flow and TypeScript


type Result = { status: 'done', payload: Object };
type Failure = { status: 'error', code: number};
           

function callback(response: Result | Failure) {
    // works, as this is present in both
    console.log(response.status);
    // does not work,
    // as we do not know if it exists, just yet
    console.log(response.payload); // ERROR
    console.log(response.code); // ERROR
           
    switch (response.status) {
        case 'done':
            // this is the special thing:
            // type system now knows, this is a Result
            console.log(response.payload);
            break;
        case 'error':
            // and this is a Failure
            console.log(response.code);
            break;
    }
}           

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

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 vs Nominal Typing

  • Nominal Typing: types are compatible when their declared types match
  • Structural Typing: types are compatible 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

Structural Typing for both TypeScript and Flow


interface NamedObject {
    name: string;
}

// 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, missing name
let namedObject: NamedObject = {
    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

Integration with raw JavaScript

3rd Party Libraries in Flow

TypeScript Declaration files

  • Much larger base for external declarations DefinitelyTyped
  • TypeScript 1.x
    • needed External Type Declarations for 3rd party libraries
    • turned out to be a major PITA
  • TypeScript 2.x
    • like Flow
    • install external type declarations transparently using npm (e.g.npm install @types/react --save)

TL'DR Part II

  • Flow: Easy to incrementally add to existing projects
    • you can choose which file to check, turn on one by one
    • which level of checking (weak/full)
    • have type annotations for IDE only without extra tooling
    • additionally and optionally use checker
  • TypeScript: Superior tooling
    • Language Server is part of compiler
    • Enables any IDE to deliver supreme support without much effort
    • WebStorm / IDEA support is close to what you can do in Java (but not quite there, yet)

Thank you!

Questions / Discussion

Oliver Zeigermann / @DJCordhose

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