Introduction to TypeScript

Mathias SchΓ€fer
9elements
2019-05-08

First, let’s talk about JavaScript (ECMAScript).

ECMAScript’s type system

πŸ§šπŸ»β€β™‚οΈ Weak πŸ§šπŸ»β€β™‚οΈ

πŸƒπŸΎβ€β™€οΈ Dynamic πŸƒπŸΎβ€β™€οΈ

πŸ§šπŸ»β€β™‚οΈ Weak vs. strong typing πŸ‹πŸ½β€β™€οΈ

Weak: Implicit type conversions

Strong: Explicit conversion necessary

(Disclaimer: There are several definitions and criteria.)

πŸ§šπŸ»β€β™‚οΈ ECMAScript:
Implicit type conversions


> 5 + "5"
'55'
> 5 + "a"
NaN

πŸ‹πŸ½β€β™€οΈ Ruby:
Explicit conversion necessary


> 5 + "5"
TypeError: String can't be coerced into Integer
from (pry):1:in `+'
> 5 + "a"
TypeError: String can't be coerced into Integer
from (pry):2:in `+'
> 5 + Integer("5", 10)
=> 10
> 5.to_s + "a"
=> "5a"

πŸ§˜πŸΎβ€β™€οΈ Static vs. dynamic typing πŸƒπŸΎβ€β™€οΈ

Static: A variable may only hold values of a certain type

Dynamic: A variable may hold arbitrary values

(Disclaimer: There are several definitions and criteria.)

πŸƒπŸΎβ€β™€οΈ Dynamic typing πŸƒπŸΎβ€β™€οΈ

The type of a variable is not known at compile time

Type may change at runtime

Type needs to be checked at runtime

Type changes at runtime


let age = 56;
age = "fifty-six";

πŸ€·πŸ»β€β™€οΈ

Type check at runtime


let age = 56;
age(); // TypeError: age is not a function

πŸ§˜πŸΎβ€β™€οΈ Static typing πŸ§˜πŸΎβ€β™€οΈ

The type of a variable is known at compile time

Type may not change at runtime

Type may be checked at compile time

πŸ§˜πŸΎβ€β™€οΈ Rust:
Type check at compile time


fn main() {
  let mut x = 56;
  x = "fifty-six";
}

πŸ§˜πŸΎβ€β™€οΈ Rust:
Type check at compile time


error[E0308]: mismatched types
 --> src/main.rs:3:9
  |
3 |     x = "fifty-six";
  |         ^^^^^^^^^^^ expected integer, found reference
  |
  = note: expected type `{integer}`
             found type `&'static str`

πŸ‹πŸ½β€β™€οΈ πŸ§˜πŸΎβ€β™€οΈ Benefits of strong & static

Developer productivity

Explicit, readable, self-documenting code

Catch a certain bugs early

Helps with complex logic

ECMAScript types

Primitive types:
Undefined, Null, Boolean, Number, String, Symbol.

Complex type:
Object.

Built-in Object types

Object, Function, Error, Date, RegExp, Array, typed arrays, (Weak)Map, (Weak)Set, Promise…

Boxed primitives (Boolean, Number, String)

TypeScript

A language that compiles to ECMAScript

TypeScript adds type information

A superset (extension) of ECMAScript

All valid ECMAScript is valid TypeScript

Open source project by Microsoft

TypeScript Compiler

Take *.ts files

Type checks

Translate to the ECMAScript target version

Output *.js files

Playground

typescriptlang.org/play/

Example


let a = 1;
let b = 2;
let c = a + b;
let d = false;
let e = "Garfield";

πŸ§ πŸ’‘

Type annotations


let a: number = 1;
let b: number = 2;
let c: number = a + b;
let d: boolean = false;
let e: string = "Garfield";

Type inference

All values have a type

A variable or property has an implicit or explicit type

Type annotations are optional

The compiler infers the type logically

Implicit types


const height = 1.7;
console.log(height.toFixed(2));
console.log(height.startsWith("1"));

Property 'startsWith' does not exist on type '1.7'.

Implicit types


const cat = { name: 'Garfield' };
// Inferred type: { name: string; }
console.log(cat.name);
console.log(cat.age);

Property 'age' does not exist on type
'{ name: string; }'.

Array types


const cats = [
  'Grumpy Cat', 'Maru', 'Lil Bub'
];

const cats: string[] = [
  'Grumpy Cat', 'Maru', 'Lil Bub'
];

string[] means array of strings

Array types


const cats = [
  'Grumpy Cat', 'Maru', 'Lil Bub'
];
cats.push(12345);

Argument of type '12345' is not assignable to parameter of type 'string'.

Function type annotations


const sum = (a, b) =>
  a + b;

const sum = (a: number, b: number): number =>
  a + b;

Parameter type check


const sum = (a: number, b: number): number =>
  a + b;
console.log(sum(5, '5'));

Argument of type '"5"' is not assignable to parameter of type 'number'.

Classes


class Cat {
  // Declare instance property
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  meow(): void {
    console.log(`${this.name} says meow!`);
  }
}
const garfield = new Cat('Garfield');
garfield.meow();

Public vs. private properties


class Cat {
  // Declare instance property
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  meow(): void {
    console.log(`${this.name} says meow!`);
  }
}
const garfield = new Cat('Garfield');
garfield.meow();

Public vs. private properties


const garfield = new Cat('Garfield');
garfield.meow();
console.log(garfield.name);

Property 'name' is private and only accessible within class 'Cat'.

Public vs. private methods


class Cat {
  private name: string;
  constructor(name: string) {
    this.name = this.sanitizeName(name);
  }
  private sanitizeName(name: string): string {
    return name.replace(/[^a-zA-Z]/g, '');
  }
  public meow(): void {
    console.log(`${this.name} says meow!`);
  }
}

Public vs. private methods


const garfield = new Cat('Garfield!');
garfield.meow(); // Garfield says meow!
garfield.sanitizeName();

Property 'sanitizeName' is private and only accessible within class 'Cat'.

Type definitions

TypeScript knows ECMAScript’s built-in types

Host environments: Node.js and browsers

Type definitions for libraries & frameworks (e.g. DefinitelyTyped)

Own type definitions

TypeScript allows to define custom types

Combining and modifying other types

AKA algebraic data types

Interfaces

Defines the abstract shape of an object


interface CatOwner {
  name: string;
  birthday: Date;
  cats: Cat[];
  adoptCat: (cat: Cat) => void
}

Interface usage


const jon: CatOwner = {
  name: 'Jon Arbuckle',
  birthday: new Date(1951, 6, 28),
  cats: [
    new Cat('Garfield')
  ],
  adoptCat(cat: Cat) {
    this.cats.push(cat);
  }
};

Class implements interface


class User implements CatOwner {
  public cats: Cat[] = []; // init
  constructor(
    public name: string,   // init
    public birthday: Date, // init
    // New instance property
    public email: Date,
  ) {}
  public adoptCat(cat: Cat) {
    this.cats.push(cat);
  }
}

Intersection types


interface Serializable {
  serialize: () => string
}
type SerializableCatOwner =
  CatOwner & Serializable;
const serialJon: SerializableCatOwner = {
  ...jon,
  serialize() {
    return JSON.stringify(this)
  }
};

Union types


class Cat {
  constructor(public name: string) {}
  meow() {
    console.log(`${this.name} says meow!`);
  }
}
class Dog {
  constructor(public name: string) {}
  woof() {
    console.log(`${this.name} says woof!`);
  }
}
type Pet = Cat | Dog;

Using a union type


class PetOwner {
  public pets: Pet[] = [];
  constructor(public name: string) {}
  adoptPet(pet: Pet) {
    this.pets.push(pet);
  }
}
const jon = new PetOwner('Jon');
jon.adoptPet(new Cat('Garfield'));
jon.adoptPet(new Dog('Laika'));

Most common union type


someType | undefined

AKA Maybe, Some

Maybe


const prefix = 'B';
const names = [ 'Anton', 'Berta', /* … */ ];

let foundName: string | undefined;
for (let i = 0, l = names.length; i < l; i++) {
  const name = names[i];
  if (name.startsWith(prefix)) {
    foundName = name
    break;
  }
}
console.log('found', foundName);

Optional properties and parameters

property?: someType is short for
property: someType | undefined


class Cat {
  constructor(public name: string) {}
  birthday?: Date;
  meow(addressee?: string) {
    const to = addressee ||Β 'everyone';
    console.log(`${this.name} says meow to ${to}`);
  }
}

Optional properties and parameters


const cat = new Cat('Maru');
cat.meow(); // … to everyone
cat.meow('you'); // … to you

Optional properties and parameters

TypeScript forces you to handle both cases


console.log(cat.birthday.getFullYear());

cat.birthday: Object is possible undefined.

Type assertions

Narrow down union types


// cat.birthday is a Date or undefined
if (cat.birthday) {
  // cat.birthday is a Date in this branch
  console.log(cat.birthday.getFullYear());
} else {
  // cat.birthday is undefined in this branch
  console.error('πŸ€·πŸ½β€β™‚οΈ');
}

πŸ§Ÿβ€β™€οΈ any type πŸ§Ÿβ€β™‚οΈ


let x: any; // undefined
x = 1;
x = '1';
x = {};

πŸ§Ÿβ€β™€οΈ any type πŸ§Ÿβ€β™‚οΈ

any means untyped code.

any is contagious.

Avoid at all cost!

Enums

A set of constants


const ORDER_NEW_STATE = 0;
const ORDER_QUEUED_STATE = 1;
const ORDER_PROCESSED_STATE = 2;
const ORDER_IN_SHIPMENT_STATE = 3;
const ORDER_DELIVERED_STATE = 4;
const ORDER_CANCELED_STATE = 5;
const order: Order = { state: ORDER_NEW_STATE };

Enums


enum OrderState {
  New,
  Queued,
  Processed,
  InShipment,
  Delivered,
  Canceled
}
interface Order { state: OrderState }
const order: Order = { state: OrderState.New };

Enums

Numerical by default (0, 1, 2…)

OrderState is compiled to an object

OrderState.New === 0 etc.

String enums:


enum OrderState { New = 'NEW', /* … */ }

Generics

A generic type has other types as variables, e.g.

Array of Cats

Map of ID & Person

Promise of User

Generics


const arrayOfCats: Cat[] = [
  new Cat('Grumpy Cat'),
  new Cat('Maru'),
  new Cat('Lil Bub')
];
const arrayOfCats: Array<Cat> = [
  new Cat('Grumpy Cat'),
  new Cat('Maru'),
  new Cat('Lil Bub')
];

Generics


const catsByName = new Map<string, Cat>();
arrayOfCats.forEach((cat) => {
  catsByName.set(cat.name, cat);
})
// Type: Cat | undefined
const cat = catsByName.get('Grumpy Cat');
console.log(cat);

Your own generics

Example: Tree structure with nodes


interface GenericNode<T> {
  data: T;
  children?: GenericNode<T>[];
}

Type variable T

Your own generics

Example: Category with sub-categories


interface Category { name: string; }
const drama: Category  = { name: 'Drama' };
const romance: Category = { name: 'Romance' };

type CategoryNode = GenericNode<Category>;
const categoryTree: CategoryNode = {
  data: drama,
  children: [
    { data: romance }
  ]
};

Generic functions

More frequent than generic types

Type information often gets lost,
the return type gets any

Input type is variable but determines output type


function compact(array) {
  const result = [];
  for (let i = 0, l = array.length; i < l; i++) {
    const item = array[i];
    if (!(item === undefined || item === null)) {
      result.push(item);
    }
  }
  return result;
}
// Inferred type: (number | null | undefined)[]
const mixedArray = [ 1, 2, undefined, null, 5 ];
// Inferred type: any[] 😧
const arrayOfNumbers = compact(mixedArray);

function compact(array: any[]): any[] {
  const result = [];
  for (let i = 0, l = array.length; i < l; i++) {
    const item = array[i];
    if (!(item === undefined || item === null)) {
      result.push(item);
    }
  }
  return result;
}
// Inferred type: (number | null | undefined)[]
const mixedArray = [ 1, 2, undefined, null, 5 ];
// Type: any[] πŸ˜•
const arrayOfNumbers = compact(mixedArray);

function compact<T>(array: T[]): T[] {
  const result = [];
  for (let i = 0, l = array.length; i < l; i++) {
    const item = array[i];
    if (!(item === undefined || item === null)) {
      result.push(item);
    }
  }
  return result;
}
// Inferred type: (number | null | undefined)[]
const mixedArray = [ 1, 2, undefined, null, 5 ];
// Type: (number | null | undefined)[] πŸ˜€πŸ˜§πŸ˜Ÿ
const arrayOfNumbers = compact(mixedArray);

function compact<T>(array: (T | undefined |Β null)[]): T[] {
  const result: T[] = [];
  for (let i = 0, l = array.length; i < l; i++) {
    const item = array[i];
    if (!(item === undefined || item === null)) {
      result.push(item); // undefined & null are excluded
    }
  }
  return result;
}
// Inferred type: (number | null | undefined)[]
const mixedArray = [ 1, 2, undefined, null, 5 ];
// Type: number[] 😍😍😍
const arrayOfNumbers = compact(mixedArray);

TypeScript compiler options

Per default, the configuration is very loose

You need to activate…

the strict mode

Strict Mode

Switch that enables several compiler options,
most importantly strictNullChecks

In loose mode:

const userName: string = undefined; // valid 😱

In strict mode:

const userName: string = undefined; // invalid 😀

Strict Mode

TypeScript is rather pointless without strict mode.

It exists to ease the migration from untyped JS to TS.

Strengths

Good entry into statically-typed programming

Expressive and correct (in strict mode)

The language and the ecosystem grows and matures

Strengths

Developer productivity (Visual Studio Code)

Self-documenting code

Easier refactoring

Type checks are a thin testing layer

Strengths

Declare the application state as interfaces →
Type checking everywhere

Works well with Redux, mobx-state-tree etc.

Strengths

Integrates with libraries, frameworks, tools

Create React App, Angular CLI, Vue CLI…

npx create-react-app my-app --typescript

Libraries and frameworks are written in TypeScript

Weaknesses

Compiler toolchain is necessary

ECMAScript vs. TypeScript evolve separately

Weaknesses

TypeScript is Work in Progress

Some things are incorrect or missing

The error messages are cryptic

Workaround: Declare types explicitly and early

Weaknesses

After compilation, it’s plain JavaScript

Type definitions may be incorrect

🌈 Typed fantasy dream land vs.
β›ˆοΈ untyped JavaScript land

Resources

typescriptlang.org

Playground

Marius Schulz’ articles

React+TypeScript Cheatsheets

Type definitions for ES, Browser, Node etc.

Questions? πŸ™‹ Remarks?

πŸ‹πŸ½β€β™€οΈ Thanks! πŸ§˜πŸΎβ€β™€οΈ

Slides:
molily.de/typescript/