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>
Adding types to your Javascript
By Abhishek Yadav
Adding types to your Javascript
- 1,286