TypeScript - an introduction

Jonas Chapuis, Ph.D. nxlogo typescript logo

A Microsoft initiative

First launched in 2012

Spearheaded by Anders Hejlsberg, C# creator

Open source (Apache 2), backed by MS, on github

Adoption

trend

Usage

usage

Compatibility

compatibility

Compiler

Language design principles

  • Use the behavior of JavaScript rather than mimic the design of existing languages.
  • Emit idiomatic JS code rather than optimize the runtime performance of programs.
  • Strike a balance between correctness and productivity in the type system.
  • No run-time type information in programs, avoid run-time metadata.
  • No additional runtime functionality: add typings to existing libraries.

Typings

  • To use any conventional JavaScript library with type-checking, you need a .d.ts type definition file, for instance jquery.d.ts
  • These are now often included in npm packages, or can be found on definitelytyped.org

Tooling

IDE support

  • Microsoft: VSCode (free, cross-plat), VisualStudio
  • JetBrains: IntelliJ IDEA, WebStorm
  • Editors: Atom, Sublime, Emacs, …
  • Online: Cloud9, Codeenvy, Alm

Build

  • Automation: via plugins (grunt, maven, gulp, gradle, sbt, ...)
  • Linter: tslint
  • Debugger: any debugger supporting source maps (e.g. Chrome Dev Tools)

Basic Types

Boolean

In [12]:
let isDone: boolean = false;
isDone
Out[12]:
false

Number

In [13]:
let integer: number = 6;
let decimal: number = 6.555;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
Out[13]:
undefined
In [14]:
integer
Out[14]:
6
In [15]:
decimal
Out[15]:
6.555
In [16]:
octal
Out[16]:
484

String

In [17]:
let color: string = "blue";
color = "red";
let favorite = `Hello, my favorite color is ${color}.`
favorite
Out[17]:
'Hello, my favorite color is red.'

Array

In [18]:
let list: number[] = [1, 2, 3];
let genList: Array<number> = [1, 2, 3];
Out[18]:
undefined
In [19]:
list
Out[19]:
[ 1, 2, 3 ]
In [20]:
genList
Out[20]:
[ 1, 2, 3 ]

Tuple

In [21]:
let t: [string, number] = ["hello", 10];
t[0] = 1
[COMPILE ERROR]
	Line 2: Type '1' is not assignable to type 'string'.

Enum

In [22]:
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
c
Out[22]:
1

Retrieve enum value from int:

In [23]:
let blue = Color[2];
blue
Out[23]:
'Blue'

Any

In [ ]:
let notSureOfTheType: any = 4;
notSureOfTheType = "maybe a string instead";
notSureOfTheType.quack();  // compiler ok, maybe it's actually a duck

arrays with untyped elements:

In [24]:
let anyList: any[] = [1, true, "free"]; // we know it's an array, but not what's inside
anyList
Out[24]:
[ 1, true, 'free' ]

void

In [ ]:
function warnUser(): void {
  alert("this is my warning message")
}

null and undefined

In [ ]:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

--strictNullChecks compiler flag

In [ ]:
function processValue(value: string): void {
  // code here assumes param is non-null
} 
processValue(null) // error TS2345: Argument of type 'null' is not assignable to parameter of type 'string'.

function processOptionalValue(value: string | undefined): void {
  // code here deals with undefined values as well
}
processOptionalValue(undefined) // compiler ok

never

In [ ]:
// Function returning never must have unreachable end point
function error(message: string): never {
  throw new Error(message);
}
In [ ]:
// Compile with --strictNullChecks
var a = []; // Type of a is never[]
a.push(5); // Error: Argument of type 'number' is not assignable to parameter of type 'never'

var b: number[] = [];
b.push(5); // compiler ok

Type assertions

In [26]:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
strLength = (someValue as string).length;
Out[26]:
16

Variable declarations

var quirks

Function scoping

In [27]:
function f(shouldInitialize: boolean) {
   if (shouldInitialize) {
     var x = 10;
   }
   return x;
}
Out[27]:
undefined
In [28]:
f(true);
Out[28]:
10
In [29]:
f(false);
Out[29]:
undefined

var quirks

variable capturing

In [30]:
for (var i = 0; i < 10; i++) {
   setTimeout(function() { console.log(i); }, 100 * i);
}
undefined // just to avoid garbage in output
Out[30]:
undefined
10
10
10
10
10
10
10
10
10
10

TypeScript's let to the rescue

Block-scoping

In [31]:
function fWithLet(shouldInitialize: boolean) {
   if (shouldInitialize) {
     let x = 10;
   }
   return x;
}
[COMPILE ERROR]
	Line 5: Cannot find name 'x'.

variable capturing

block scoping, one capture at each iteration

In [32]:
for (let i = 0; i < 10; i++) {
   setTimeout(function() { console.log(i); }, 100 * i);
}
undefined // just to avoid garbage in output
Out[32]:
undefined
0
1
2
3
4
5
6
7
8
9

Immutable values: const

In [33]:
const numLivesForCat = 9;
const kitty = {
  name: "Aurora",
  numLives: numLivesForCat,
}

// Error
kitty = {
  name: "Danielle",
  numLives: numLivesForCat
};

// all "okay" – use readonly instead 
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
[COMPILE ERROR]
	Line 8: Cannot assign to 'kitty' because it is a constant or a read-only property.

Destructuring

Arrays

In [34]:
const input = [1, 2];
let [first, second] = input;
console.log(first, second);

[first, second] = [second, first]; // swap variables
console.log(first, second);
1 2
2 1
Out[34]:
undefined

Tuples

In [35]:
let t: [string, number] = ["hello", 10];
let [fst, scd] = t
console.log(fst, scd)
hello 10
Out[35]:
undefined

Objects

In [36]:
let o = {
  someA: "foo",
  someB: 12,
  someC: "bar"
};
o
Out[36]:
{ someA: 'foo', someB: 12, someC: 'bar' }
In [37]:
let { someA, someB } = o;
Out[37]:
undefined
In [38]:
someA
Out[38]:
'foo'
In [39]:
someB
Out[39]:
12
In [40]:
let { someA: newName1, someB: newName2 } = o;
Out[40]:
undefined
In [41]:
newName1
Out[41]:
'foo'
In [42]:
newName2
Out[42]:
12

Spreading

In [43]:
let x = [1, 2];
let y = [3, 4];
let bothPlusSome = [0, ...x, ...y, 5];
bothPlusSome
Out[43]:
[ 0, 1, 2, 3, 4, 5 ]
In [ ]:
let defaultOptions = { food: "spicy", price: "$$", ambiance: "noisy"};
let searchOptions = { ...defaultOptions, food: "rich"};
searchOptions

Iterables and iterators

In [45]:
let someArray = [1, "string", false];

// iterate on values
for (let entry of someArray) {
    console.log(entry);
}
1
string
false
Out[45]:
undefined
In [47]:
let someObject = {
    foo: "bar",
    bar: "foo"
}

// iterate on object keys
for (let key in someObject){
    console.log(key);
}
foo
bar
Out[47]:
undefined

An object is deemed iterable if it has an implementation for the Symbol.iterator property:

In [48]:
someArray[Symbol.iterator]
Out[48]:
[Function: values]

you can always go functional with most of iterable types as well, with map or forEach

In [49]:
someArray.map(v => v.toString());
Out[49]:
[ '1', 'string', 'false' ]

Generators

In [50]:
function* idMaker(){
  let index = 0;
  while(index < 3)
    yield index++;
}

let gen = idMaker();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }
Out[50]:
undefined

Interfaces

Type-checking focuses on the shape that values have. This is called “duck typing” or “structural subtyping”, but done at compile time.

Plain Javascript

In [51]:
let obj = {size: 10, label: "Size 10 Object"};

// using plain JavaScript
function printLabelledJs(labelled) {
  console.log(labelled.label);
}
printLabelledJs(obj);
Size 10 Object
Out[51]:
undefined

TypeScript static 'duck-typing'

In [52]:
let foo = {label : "foo"}
printLabelledTs(foo);
[COMPILE ERROR]
	Line 2: Cannot find name 'printLabelledTs'. Did you mean 'printLabelledJs'?

TypeScript interface definition

In [53]:
// using TypeScript interface definition
interface Labelled {
  label: string;
}
function printLabelledItfc(labelled: Labelled) {
  console.log(labelled.label);
}
Out[53]:
undefined

Extending and implementing interfaces

In [54]:
interface Colored {
    color : string;
}

interface Framed {
    stroke: number;
}

interface Shape extends Colored, Framed {
    width: number;
    height: number;
}

// class declaration, implementing the combined interface
//  - more about classes later
class Square implements Shape {
    constructor(public width: number, public height: number, public color: string, public stroke: number){}
}
Out[54]:
[Function: Square]

Optional properties

In [57]:
interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}
Out[57]:
undefined
In [58]:
createSquare({color: "black"});
Out[58]:
{ color: 'black', area: 100 }

Read-only properties

In [59]:
interface Point {
    readonly x: number;
    readonly y: number;
}
Out[59]:
undefined
In [60]:
let p1: Point = { x: 10, y: 20 };
p1.x = 5;
[COMPILE ERROR]
	Line 2: Cannot assign to 'x' because it is a constant or a read-only property.

Side note: read-only arrays

In [61]:
let ro: ReadonlyArray<number> = [1, 2, 3, 4];
ro[0] = 0;
[COMPILE ERROR]
	Line 2: Index signature in type 'ReadonlyArray<number>' only permits reading.

Function types

In [62]:
interface SearchFunc {
    (source: string, part: string): boolean;
}
Out[62]:
undefined
In [63]:
let mySearch: SearchFunc;
mySearch = function(source: string, part: string): boolean {
    let result = source.search(part);
    return result > -1;
}
mySearch("the quick brown fox jumps over the lazy dog", "jumps")
Out[63]:
true

Indexable types

In [64]:
interface AgeMap {
    [name: string]: number; // key type can only be string or number!
}
Out[64]:
undefined
In [65]:
let ages: AgeMap = { "Jonas": 25, "Alfred": 50 };
ages["Jonas"];
Out[65]:
25
In [66]:
// when targetting ES6, an alternative which allows for any key type is:
interface NameKey {
    firstName: string,
    lastName: string    
}

let ageMap = new Map<NameKey, number>();
Out[66]:
undefined
In [67]:
ageMap.set({firstName:"jonas", lastName: "chapuis"}, 25)
Out[67]:
Map { { firstName: 'jonas', lastName: 'chapuis' } => 25 }

Classes

Moving away from prototypes towards traditional object-orientation

Example class: Animal

In [68]:
abstract class Animal {
    name: string;
    
    constructor(theName: string) { this.name = theName; }
    
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
    
    abstract makeSound(): void; // must be implemented in derived classes
}
Out[68]:
[Function: Animal]
In [69]:
let animal = new Animal("Elephant");
[COMPILE ERROR]
	Line 1: Cannot create an instance of the abstract class 'Animal'.

Concrete derived class: Snake

In [70]:
class Snake extends Animal {
    constructor(name: string) { super(name); }
    
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
    
    makeSound() {
        $$.html("<audio src='snake.mp3' autoplay/>");
    }
}
Out[70]:
[Function: Snake]

Horse: another derived class of Animal

In [71]:
class Horse extends Animal {
    constructor(name: string) { super(name); }
    
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
    
    makeSound() {
        $$.html("<audio src='horse.wav' autoplay/>");
    }
}
Out[71]:
[Function: Horse]

Let's play with our animals...

In [72]:
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
Out[72]:
undefined
In [ ]:
sam.makeSound();
In [ ]:
tom.makeSound();

Modifiers

In [73]:
class Vehicle { 
    // more compact version to declare class members (using modifier)
    constructor(protected model:string, protected manufacturer:string, public description: string) {}
}

class Car extends Vehicle {
    constructor(model: string, manufacturer : string, private tires:string) {
        super(model, manufacturer, model + ", " + manufacturer + ", " + tires);
    }
}
Out[73]:
[Function: Car]
In [74]:
let car = new Car("S", "Tesla", "unknown");
car
Out[74]:
Car {
  model: 'S',
  manufacturer: 'Tesla',
  description: 'S, Tesla, unknown',
  tires: 'unknown' }
In [75]:
car.manufacturer
[COMPILE ERROR]
	Line 1: Property 'manufacturer' is protected and only accessible within class 'Vehicle' and its subclasses.
In [76]:
car.tires
[COMPILE ERROR]
	Line 1: Property 'tires' is private and only accessible within class 'Car'.

Readonly

In [77]:
class Immutable{
    constructor(readonly data: string) {};
}
let immutable = new Immutable("some data");
immutable.data = "foobar"
[COMPILE ERROR]
	Line 5: Cannot assign to 'data' because it is a constant or a read-only property.

Accessors

In [78]:
class Employee {
    private _fullName: string;

    get fullName(): string {
        console.log("returning fullName...");
        return this._fullName;
    }

    set fullName(newName: string) {
        console.log("updating fullName...");
        this._fullName = newName;
    }
}
Out[78]:
[Function: Employee]
In [79]:
let employee = new Employee();
employee.fullName = "Bob Smith";
employee.fullName
updating fullName...
returning fullName...
Out[79]:
'Bob Smith'

Functions

Function declarations

Named functions

In [3]:
function add(x: number, y: number): number {
    return x + y;
}
add(1, 1)
Out[3]:
2

Anonymous functions

In [80]:
let myAdd = function(x: number, y: number): number { return x + y; };
myAdd(1, 1)
Out[80]:
2

Arrow functions

In [4]:
let myArrowAdd = (x: number, y: number) => { return x + y};
myArrowAdd(1, 1)
Out[4]:
2

Parameters

In [ ]:
// default parameters
function check(text: string, timeout: number = 100): boolean {
    return true;
}

// optional parameters
function validate(text: string, options?: string): boolean {
    return true;
}

// variadic parameters
function searchContents(text: string, ...contents: string[]): string[] {
    return [];
}

Overloads

In [6]:
function serialize(o: number): string;
function serialize(o: string): string;
function serialize(o): string {
  return JSON.stringify(o);
}
serialize(4);
Out[6]:
'4'
In [7]:
serialize('text');
Out[7]:
'"text"'
In [8]:
serialize(true)
[COMPILE ERROR]
	Line 1: Argument of type 'true' is not assignable to parameter of type 'string'.

Arrow functions and closures

In [9]:
class AlertsFactory {
   private alertsCount: number = 0;
   createAlerterFor(problem: string) {
    return () => {
        console.log("MAXIMUM ALERT: "+problem); 
        this.alertsCount++;
        console.log("total number of alerts issued: " + this.alertsCount);
     }
   }
}
Out[9]:
[Function: AlertsFactory]
In [10]:
let factory = new AlertsFactory();
let alerter1 = factory.createAlerterFor("crash");
let alerter2 = factory.createAlerterFor("bug");
alerter1();
alerter2();
alerter1();
MAXIMUM ALERT: crash
total number of alerts issued: 1
MAXIMUM ALERT: bug
total number of alerts issued: 2
MAXIMUM ALERT: crash
total number of alerts issued: 3
Out[10]:
undefined

Generics

Generic functions

In [82]:
function printArray<T>(array: Array<T>) {
   array.forEach(el => console.log(el));
   console.log("total elements count: "+array.length);
}
Out[82]:
undefined
In [83]:
printArray(["foo", "bar"])
foo
bar
total elements count: 2
Out[83]:
undefined

Generic types

In [84]:
interface Entity {
    id(): string;
 }

 interface Repository<T extends Entity> {
    getEntityWithId(id: string): T;
 }

 class DummyEntity implements Entity {
    constructor(private _id: string, public payload: string){}
    
    id(): string {
       return this._id;
    }
 }
Out[84]:
[Function: DummyEntity]
In [85]:
class DummiesRepository implements Repository<DummyEntity> {
     private entitiesPerId = new Map<string, DummyEntity>()
     
     constructor() {
         this.entitiesPerId["foo"] = new DummyEntity("foo", "some foo payload");
         this.entitiesPerId["bar"] = new DummyEntity("bar", "some bar payload");
     }
     
     getEntityWithId(id: string): DummyEntity {
        return this.entitiesPerId[id];
     }
 }

 let dummies = new DummiesRepository();
 dummies.getEntityWithId("foo");
Out[85]:
DummyEntity { _id: 'foo', payload: 'some foo payload' }

Discriminated unions

Pattern matching with TypeScript!

In [86]:
interface Branch {
    kind: "branch"; // string literal type
    left: Tree;
    right: Tree;
}
interface Leaf {
    kind: "leaf";
    value: number;
}
type Tree = Branch | Leaf

function sumLeaves(tree: Tree): number {
    switch(tree.kind){
        case "branch": return sumLeaves(tree.left) + sumLeaves(tree.right);
        case "leaf": return tree.value;
    }
}
Out[86]:
undefined
In [87]:
sumLeaves(
    { 
        kind: "branch",
        left: {
            kind: "branch",
            left: { kind: "leaf", value: 1},
            right: { kind: "leaf", value: 1}
        },
        right: { kind:"leaf", value: 1}
    }
)
Out[87]:
3

Index types

Dynamic lookup, but with compile-type safety!

In [88]:
function getProperty<T, K extends keyof T>(o: T, key: K): T[K] {
    return o[key]; // o[key] is of type T[K]
}
Out[88]:
undefined
In [89]:
interface Person {
    name: string;
    age: number;
}
Out[89]:
undefined
In [90]:
type PersonKeys = keyof Person  // "name" | "age" (union type of string literals)
type PersonLookupType = Person[PersonKeys]  // string | number (union type of Person's properties type)
function getPersonProperty<Person, PersonLookupType>(o: Person, key: PersonKeys): PersonLookupType {
    return o[key]; // returns the property, with type-safety on key and proper return type
}
Out[90]:
undefined
In [91]:
let person: Person = {
    name: 'Jonas',
    age: 25
};
Out[91]:
undefined
In [92]:
getProperty(person, 'name');
Out[92]:
'Jonas'
In [93]:
getProperty(person, 'age');
Out[93]:
25
In [94]:
getProperty(person, 'unknown');
[COMPILE ERROR]
	Line 1: Argument of type '"unknown"' is not assignable to parameter of type '"name" | "age"'.

Symbols

TypeScript's "type-level" or "aspect-oriented" programming

Symbols are a way to declare global identifiers for "aspects" that objects can implement - code can then rely on these aspects being present. Example of pre-built symbols:

In [95]:
Symbol.iterator // used by for..of, returns the default iterator for an object
Out[95]:
Symbol(Symbol.iterator)
In [96]:
Symbol.hasInstance // used by instanceof operator, determines if an object is of one class
Out[96]:
Symbol(Symbol.hasInstance)

Example: custom Iterable class

It's just about providing the [Symbol.iterator] property:

In [97]:
class IdMaker {
 [Symbol.iterator] = function*() {
      var index = 0;
       while(true && index<3) {
        yield index++;
      }
    }
}

var maker = new IdMaker();
for (let id of maker) { // note: when targetting ES5, use the --downlevelIteration compiler flag
  console.log(id);
}
0
1
2
Out[97]:
undefined

For more information...

TypeScript official documentation is pretty good and mostly up-to-date (!): many code examples and the structure of this presentation follow the TypeScript Handbook

For more detailed and up-to-date information, directly on the github repo, the 2000+ issues in particular