Oliver Zeigermann / @DJCordhose
If you are looking for the most recent and up-to-date version of my comparisons on Typed JavaScript you have just found it :)
type annotations / inferred types
Anders Hejlsberg@Build2016: Big JavaScript codebases tend to become "read-only".
Recently published survey on the state of JavaScript
// 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}`;
}
}
// 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}`;
}
}
Those basic features help with documentation, refactoring, and IDE support
let
-- declaration using type
foo : String
foo = "yo"
-- Error: everthing is const, can not re-assign
foo = "yo yo"
foo2 : String
-- Error: `The definition of `obj2` does not match its type annotation.`
foo2 = 10
let
-- type annotations are optional, can be inferred
sayIt : String -> String
sayIt what =
"Saying: " ++ what
said : String
said = sayIt obj
No classes and methods in elm
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
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
// 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 (in this case we actually do)
fooed!.toString();
Only applies to TypeScript 2.x
Only works when strictNullChecks option is checked
All types nullable by default in TypeScript 1.x
There neither is null
nor undefined
in elm
Rather Maybe plus pattern matching
-- Maybe is predefined
-- http://package.elm-lang.org/packages/elm-lang/core/latest/Maybe
type Maybe a = Nothing | Just a
foo : Int -> Maybe String
foo num =
if num > 10 then
Just "cool"
else
Nothing
-- pattern matching (need to match all cases)
case (foo 11) of
Just message -> message
Nothing -> ""
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'));
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 :)
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
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);
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"));
}
Consider
class Dog { woof() { } }
const animals = [];
animals.push(new Dog());
both TypeScript and Flow know this is safe, as we have only added Dogs so far
animals.forEach((animal: Dog) => animal.woof());
Adding Cats later and thus changing array type later
class Cat { meow() { } }
animals.push(new Cat());
does not affect TypeScript (correct), but makes Flow fail
does not have classes or subtypes
has Records (like JavaScript Objects) and generic data structures (e.g. List)
type alias Animal = { name : String }
someAnimal1 = { name = "Patrick"}
animals : List Animal -- generic data structure
animals = [ someAnimal1, someAnimal2, ... ]
type alias Cat = { name : String, coatColor : String }
cats : List Cat
cats = [ someCat1, someCat2, ... ]
-- sure
moreAnimals : List Animal
moreAnimals = animals
-- Error: Looks like a record is missing the `coatColor` field.
evenMoreAnimals : List Animal
evenMoreAnimals = cats
-- nope, same problem
moreCats : List Cat
moreCats = animals
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
TypeScript and flow: same as JavaScript (const optional, immutable via lib)
TypeScript: readonly for properties
Elm: everything always immutable and const
Central Question: If everything always immutable and const, how do you make modifications?
Answer:
type alias Cat = { name : String, coatColor : String, age: Int}
someCat = { name = "Purry", age = 2, coatColor = "gray"}
haveBirthday : Cat -> Cat
haveBirthday cat =
-- make a copy, but with changed age
{ cat | age = cat.age + 1 }
agedCat : Cat
agedCat = haveBirthday someCat
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');
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
function callback(response: Response) {
// 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;
}
}
simple and concise union types
type Response = Result String | Failure Int
switching over union type alternatives using pattern matching
callback : Response -> String
callback response =
-- pattern matching
case response of
Result payload -> payload
Failure code ->
if code >= 400 && code < 500 then "you messed up"
else "we messed up"
usage
callback (Result "response")
-- response
callback (Failure 404)
-- you messed up
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"
};
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"
};
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"
};
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
Starting from 2016.3
My biased recommendation
Oliver Zeigermann / @DJCordhose
http://djcordhose.github.io/flow-vs-typescript/elm-flow-typescript.html