Adding types to your Javascript

An overview of Typescript, FlowType and Elm

The agenda

Types in JS

Things we expect from a sensible type system, and how Typescript and Flow implement them.

  • Type annotations and checking
  • Type inference and incremental introduction
  • Types for objects
  • Types for functions
  • Union types - the Maybe type, and the Response type
  • Type checks within branches
  • Generics

And a little theory -

  • soundness
  • nominal vs structural                                                         

Type annotations

Type annotations should be:

  • expressive
  • concise
  • have reasonable set of primitive types
  • have the any type

 

Types in JS

Type annotations - primitives

Types in JS

// Typescript

let n: number = 12;
let str: string = "abc";
let t: boolean = true;

let x: null = null;
let y: undefined = undefined;
// @flow

let n: number = 12;
let str: string = "abc";
let t: boolean = true;

let x: null = null;
let y: void = undefined;
  • Syntax very similar - variable-name colon type
  • Both have the null type
    • (but meaning varies)

Type annotations - Array

// Typescript

let arr1: number[] = [1,2,3];
let arr2: Array<number> = [1,2,3];


// The tuple
let tup1: [string, number, boolean];
tup1 = ["Abhi", 92, false]
// @flow

let arr1: number[] = [1,2,3];
let arr2: Array<number> = [1,2,3];


// The tuple
let tup1: [string, number, boolean];
tup1 = ["Abhi", 92, false]
  • Array types syntax also similar
  • Tuples can also be described

Types in JS

Type annotations - any type

// Typescript

let notSure: any;

notSure = 1
notSure = "abc"
notSure = null
notSure = undefined
// @flow

let notSure: any;

notSure = 1
notSure = "abc"
notSure = null
notSure = undefined
  • If you're not sure about the type, use any
  • It is an opt-out - the type-checker doesn't check them
  • Useful for gradually introducing types

Types in JS

Type annotations

Types in JS

Type annotations are -

  • Intuitive - easy to get started
  • Concise (so far)
  • Easy to opt-out

For both Typescript and Flow

The syntax is also very similar

The type checker

Types in JS

Typescript-

  • Install
  • File to use .ts extension
  • tsc filename.js
  • Or use plugins
  • Or use VS code

Trying out:

https://www.typescriptlang.org/play/index.html

Flow -

  • Install
  • Mark the file with @flow directive
  • yarn run flow
  • Or use plugins

 

Trying out:

https://flow.org/try/

The type checker

Types in JS

Typescript-

  • Fast √
  • Helpful error messages √
  • Catches all errors ~not-quite~
    • Not sound by design -  favors practicality

Flow -

  • Fast √
  • Helpful error messages ~not-quite~
  • Catches all errors √
    • Sound - favors correctness

The type checker

Types in JS

// Typescript

let num: number;
num = null  // No error by default

// Use the --strictNullChecks flag 
// to catch these errors
// @flow

let num: number;
num = null  // Error
  • Typescript considers the null and undefined as sub-types of all types. That is, null and undefined can be assigned to variables of any type
  • This is by design. Can be disabled
  • Useful for gradually introducing types

Type for Objects

  • We should be able to annotate types for custom structure
  • In Javascript, often maps are represented as objects
  • Many libraries use options-bags objects

 

Types in JS

Types in JS

// Typescript

interface Score {
  player: string;
  runs: number;
}

let s1:Score, s2:Score, s3:Score;

s1 = { player: 'Abhi', runs: 20 } // √
s2 = { player: 'Abhi' }           // x
s3 = { player: 'Abhi', runs: 20,  // x 
       wickets: 2 }
// @flow

interface Score {
  player: string,  // <- comma here
  runs: number
}

let s1:Score, s2:Score, s3:Score;

s1 = { player: 'Abhi', runs: 20 } // √
s2 = { player: 'Abhi' }           // x
s3 = { player: 'Abhi', runs: 20,  // √
       wickets: 2 }

  • The syntax is slightly different
  • Extra properties are allowed in Flow
  • Both allow declaring optional properties
  • Both allow readonly properties
  • Both seal the object

Type for Objects - interface

Types in JS

// Typescript

var obj = {
  foo: 1
}

obj.bar = 123 // x
obj.foo = 456 // √
// @flow

var obj = {
  foo: 1
}

obj.bar = 123 // x
obj.foo = 456 // √

Type for Objects - sealed objects

Types for Functions

  • We need -
    • Type annotations for return values
    • Type annotations for arguments
    • Support for optional arguments
    • A function type

Types in JS

Types in JS

// Typescript


function incr(x: number): number {
  return x+1
}

incr(2)    // √
incr('2')  // x
// @flow

function incr(x: number): number {
  return x+1
}

incr(2)    // √
incr('2')  // x
  • Typescript considers the null and undefined as sub-types of all types. That is, null and undefined can be assigned to variables of any type
  • This is by design. Can be disabled
  • Useful for gradually introducing types

Types for Functions

Types in JS

// Typescript


function incr(x: number): number {
  return x+1
}

incr(2)    // √
incr('2')  // x
incr()     // x
// @flow

function incr(x: number): number {
  return x+1
}

incr(2)    // √
incr('2')  // x
incr()     // x
  • Function calls without correct number of arguments is reported as error (unlike default Javascript behavior)
  • Optional arguments can be annotated 

Types for Functions

Types in JS

// Typescript

// x is optional
function incr(x?: number): number {
  if (x == undefined) return 1;
  return x+1
}

incr(1)     // √
incr('1')   // X
incr()      // √
incr(null)  // X


function incr1(x: number): number {
  return x+1  // Error: possibly undefined
}
// @flow

// x is optional
function incr(x?: number): number {
  return x+1
}


incr(2)    // √
incr('2')  // x
incr()     // √
incr(null) // x
  • Type checkers often miss out some interesting errors

Types for Functions

Types in JS

// Typescript

function incr(x: number): number {
  if (x == 1) return null;  // Error
  return x+1
}


// Error only with 
// --strictNullChecks flag

// @flow


function incr(x: number): number {
  if (x == 1) return null; // Error
  return x+1
}
  • Flow is more strict on handling null and undefined
  • Typescript considers null to be part of every type

Types for Functions

Types in JS

// Typescript

function onClick(callback: (element: string) => string){
  callback('.my-class')
}

onClick(function (el) {
  return 'ok:' + el
})


// onClick takes ONE callback function
// The callback function takes a string argument 
// and returns a string


// This can be made to look better
// @flow

Typescript

Types for Functions

Types in JS

// Typescript

type CallBackType = (element: string) => string

function onClick(callback: CallBackType): void{
  callback('div')
}

onClick(function (el) {
  return 'ok'
})
// @flow

  • type is for type aliases
  • When the type definition is too long, cumbersome or noisy, use an alias

Types for Functions

Types in JS

// Typescript

type Elem = string
type Status = string
type CallBackType = (element: Elem) => Status

function onClick(callback: CallBackType): void{
  callback('div')
}

onClick(function (el) {
  return 'ok'
})
// @flow

Types for Functions

  • Also use type alias when it describes the value better

Types in JS

//
// @flow


function onClick(callback: (string) => string){
  callback('div')
}

onClick(function (el) {
  return 'ok'
})

Flow

  • Function type looks cleaner because of the terse argument syntax

Types for Functions

Types in JS

//
// @flow

type Elem = string
type Status = string
type CallbackType = (Elem) => Status

function onClick(callback: CallbackType): void{
  callback('div')
}

onClick(function (el) {
  return 'ok'
})

Flow

  • Aliases can be used here too, syntax is the same

Types for Functions

Types in JS

// Typescript

// Valid
type FType1 = (x: number) => number
type Ftype2 = (x: number, str: string) => number  

type Ftype3 = (number) => number
type Ftype4 = (number, string) => number

// Invalid. specify the variables
type Ftype99 = (number, number) => number


// Valid
type Ftype5 = (number, Ftype2) => number
type Ftype6 = (number, Ftype2) => Ftype4

// @flow

// Valid
type FType1 = (x: number) => number
type Ftype2 = (x: number, str: string) => number  

type Ftype3 = (number) => number
type Ftype4 = (number, string) => number

// Also valid
type Ftype99 = (number, number) => number


// Valid
type Ftype5 = (number, Ftype2) => number
type Ftype6 = (number, Ftype2) => Ftype4

Types for Functions

  • Flow syntax seems more concise
  • Higher order functions can be described.

Types in JS

// Typescript

// The Function type

function onClick2(callback: Function) {
  callback();
}
// @flow



function onClick2(callback: Function) {
  callback('span');
}

Types for Functions

  • Both have a Function type as well
  • Its like any - most of the type checks are skipped for it

Types in JS

Types for Functions

More:

  • Optional arguments
  • Handling of this
  • Generics

Types in JS

Union Types

Useful when -

  • When the value could be absent (null or undefined)
  • When the structure is depends on run-time (Ajax response)

Types in JS

Union Types

// Typescript

type NBT = number | boolean | string

function toString(x: NBT): string {
  return String(x)
}
// @flow

type NBT = number | boolean | string

function toString(x: NBT): string {
  return String(x)
}
  • A type that is a mix of several types
  • It can hold values of any of its constituent types

Types in JS

Union Types - the leftPad

// Typescript


function leftPad(x: number | string  ) {

  let lp: string
  
  if (typeof (x) == "number") {
    lp = String(x)
  } else {
    lp = x
  }

  let lp1: string = lp + ' px';
  actualLeftPadding(lp1);
}
// @flow

  • The left-pad
  • All the possible types of input should be handled

Types in JS

Union Types - the leftPad

// Typescript


function leftPad(x: number | string | null ) {

  let lp: string
  
  if (typeof (x) == "number") {
    lp = String(x)
  } else {
    lp = x  // Error here
  }

  let lp1: string = lp + ' px';
  actualLeftPadding(lp1);
}
// @flow

  • Bugs introduced by adding input types will be caught

Types in JS

Union Types - List.head issue

// Typescript


function head(list: string[]): string {  
  return list[0]  // undefined for []
}
// @flow


function head(list: string[]): string {  
  return list[0]  // undefined for []
}
  • The List.head issue
  • The problem in the above is not caught by both
    • (This is also somewhat deliberate)

Types in JS

Union Types - Maybe

// Typescript

type MaybeString = string | null | undefined

function head(list: string[]): MaybeString {  
  return list[0]  // undefined for []
}
// @flow

type MaybeString = string | void;

function head(list: string[]): MaybeString {  
  return list[0]  // undefined for []
}
  • Better use the Maybe type
  • Such that any dependent functions can be safe

Types in JS

Union Types - Maybe

// Typescript

type MaybeString = string | null | undefined

function head(list: string[]): MaybeString {  
  return list[0]
}

function topScorer(): string {

  let list: string[];

  // list = fetch(...)

  return head(list); // Error here. 
}
// @flow

  • Any function that calls head must handle all the types within Maybe

Types in JS

Union Types - Maybe

// Typescript

type MaybeString = string | null | undefined

function head(list: string[]): MaybeString {  
  return list[0]
}

function topScorer(): string {

  let list: string[] = [];
  // list = fetch(...)

  let top: MaybeString = head(list);

  if (top == null) return '';
  if (top == undefined) return '';

  return top;
}
// @flow

function head(list): ?string {
  return list[0]
}

function topScorer(): string {

  let str: ?string = ''
  let list: string[] = []
  
  str = head(list)
  
  if (str == null) return ''
  return str
}
  • Any function that calls head must handle all the types within Maybe
  • Flow has a built-in maybe string type - ?string

Types in JS

Union Types - Response type

// Typescript

// Notice the type of success
type HttpSuccess = { success: true,  data:  string }
type HttpFail    = { success: false, error: string }

type HttpResponse = HttpSuccess | HttpFail

function handleResponse(response: HttpResponse) {
  let data: string;
  let err: string;

  if (response.success) 
  {
    data = response.data  // √
    console.log(data)
  } 
  else 
  {
    err = response.error  // √
    console.log(err)
  }
}
// @flow



// <= Exact same code works
  • Disjoint unions for handling http response

Types in JS

-- ELM

type User = Anonymous | Named String

userPhoto : User -> String
userPhoto user =
  case user of
    Anonymous ->
      "anon.png"

    Named name ->
      "users/" ++ name ++ ".png"
  
// @flow

Union Types - Elm example

  • From Elm documentation

Types in JS

-- ELM

type Result error value
  = Err error
  | Ok value


view : String -> Html msg
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
      span [class "error"] [text msg]

    Ok age ->
      text "OK!"
// @flow

Union Types - Elm Result type

  • From Elm documentation

Types in JS

Generics

Useful when -

  • When the logic is applicable to a variety of types
  • When the type itself is a parameter

Types in JS

Generics - identity

// Typescript

function identity<T>(x: T): T {
  return x
}

function identity1(x: any): any {
  return 123
}

let str: string = identity<string>("hello") // √
let str1: string = identity("hi") // T is inferred
// @flow

function identity<T>(x: T): T {
  return x
}
  • identity is a function that takes a value, and returns the same value
  • The type information of the value is preserved.
  • identity1 wouldn't be able to catch a wrong implementation

Types in JS

Generics - array

// Typescript

function reverse<T>(arr: T[]): T[] {
  return arr.reverse()
} 
// @flow

function reverse<T>(arr: T[]): T[] {
  return arr.reverse()
} 
  • reverse here is a function that takes an array and reverses it
  • The type of the contents doesn't matter. Hence generic

Types in JS

Generics - Maybe

// Typescript


// type MaybeString = string | null | undefined
type Maybe<T> = T | null | undefined;

function head(list: string[]): Maybe<string> {  
  return list[0]
}

function topScorer(): string {

  let list: string[] = [];
  // list = fetch(...)

  let top: Maybe<string> = head(list);

  if (top == null) return '';
  if (top == undefined) return '';

  return top;
}
// @flow

  • This gives us a more generic Maybe type
  • That we can combine with our own interfaces

Types in JS

Generics and disjoint unions - Http Response

// Typescript

type HttpSuccess<T1> = { success: true,  data: T1 }
type HttpFail<T2>    = { success: false, error: T2 }

type MyData = { name: string, score: number }
type MyError = { code: number, msg: string }

type HttpResponse = HttpSuccess<MyData> | HttpFail<MyError>

function handleResponse(response: HttpResponse) {

  let data: MyData;
  let err: MyError;

  if (response.success) 
  {
    data = response.data  // √
    console.log(data)
  } 
  else 
  {
    err = response.error  // √
    console.log(err)
  }
}
// @flow

Types in JS

Conclusion

Typescript

  • Type system - powerful
  • Tolling - excellent (VS-code)
  • Community - more active and open
  • Useful on almost all projects (even React)

Flow

  • Type system -  powerful
  • Tooling - sufficient
  • Community - okay
  • Better to use with React projects  (react-boilerplate)

Recommendations

  • Use types on all production projects
  • If unsure, use Typescript

Other comments

  • Learning curve is gentle
  • Incremental introduction, one file at a time, is the preferred approach

</end>

Made with Slides.com