TypeScript

Introduction

Miguel Cobá / @MiguelCobaMtz

TypeScript

JavaScript that scales

TypeScript is a typed superset of Javascript that compiles to plain JavaScript

Any browser. Any host. Any OS. Open source

Why?

Making JavaScript scale

Easier to build and maintain medium to large applications

Advantages

Great tooling enabled by static types

  • Statement completion
  • Go to definition
  • Refactorings

Advantages

Features from the future, today

  • Classes
  • Lambda function

Install


npm install -g typescript
						

Verify installation


tsc -v
Version 1.8.10
						

Language

Type annotations

Lightweight ways to record the intended contract of the function or variable

Basic Types

boolean


let isEnabled: boolean = false;
						

number


let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
						

string

Normal strings

  • Delimited by double quotes (") or single quotes (')

let name: string = "John";
name = 'John Smith';
						

string

Template strings

  • Delimited by (`)
  • Can contain embedded expressions: ${ expr }

let fullName: string = `Miguel Cobá`;
let age: number = 36;
let greeting: string = `Hello, my name is ${ fullName }.

I will be ${ age + 1 } years old next month.`
						

array

All the elements must be of the same type

Type + []


let list: number[] = [1, 2, 3];
						

Generic array type: Array<elemType>


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

tuple

Allow you to express an array where the type of a fixed number of elements is known, but need not be the same


// Declare a tuple type for a pair of string and number
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
						

enum

An enum is a way of giving more friendly names to sets of numeric values


// By default, enums begin numbering their members starting at 0
enum Color {Red, Green, Blue};
let c: Color = Color.Green;

// You can change this by manually setting the value of one of the members
enum Color {Red = 1, Green, Blue};
let c: Color = Color.Green;

// manually set them
enum Color {Red = 1, Green = 2, Blue = 4};
let c: Color = Color.Green;
						

any

Allows to opt-out of type-checking and let the values pass through compile-time checks


let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

let list: any[] = [1, true, "free"];

list[1] = 100;
						

void

Opposite of any: the absence of having any type at all


function warnUser(): void {
    alert("This is my warning message");
}

// Void variables are not useful because only undefined or null can be assigned to them:
let unusable: void = undefined;
						

Type assertions

When you know the type of some entity could be more specific than its current type

Is like a type cast in other languages

Angle bracket syntax


let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
						

as syntax


let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;
						

Variable declarations

var declarations

var declarations are accessible anywhere within their containing function, module, namespace, or global scope regardless of the containing block

Some people call this var-scoping or function-scoping


function f(shouldInitialize: boolean) {
  if (shouldInitialize) {
    var x = 10;
  }

  return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'
            

let declarations

Use lexical-scoping or block-scoping

Block-scoped variables are not visible outside of their nearest containing block or for-loop


function f(input: boolean) {
    let a = 100;

    if (input) {
        // Still okay to reference 'a'
        let b = a + 1;
        return b;
    }

    // Error: 'b' doesn't exist here
    return b;
}
						

let declarations

They can’t be read or written to before they’re actually declared


a++; // illegal to use 'a' before it's declared;
let a;
						

Re-declarations and Shadowing 1

Valid with var


// All the x refer to same variable in function scope
function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}
						

Re-declarations and Shadowing 2

Invalid with let


let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope

function f(x) {
    let x = 100; // error: interferes with parameter declaration
}

function g() {
    let x = 100;
    var x = 100; // error: can't have both declarations of 'x'
}
						

Re-declarations and Shadowing 3

Redeclaration is valid if in a different nested block


function f(condition, x) {
    if (condition) {
        let x = 100;    // shadowing
        return x;
    }

    return x;
}

f(false, 0); // returns '0'
f(true, 0);  // returns '100'
						

const declarations 1

They are like let declaration

Their value cannot be changed once they are bound


const numLivesForCat = 9;
						

const declarations 2

Doesn't mean that the values they refer to are immutable


const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

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

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
						

let vs. const

Principle of least privilege: all declarations other than those you plan to modify should use const

Destructuring

The destructuring assignment syntax is an expression that makes it possible to extract data from arrays or objects into distinct variables

Arrays


let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
						

Existing variables


// swap variables
[first, second] = [second, first];
						

Function parameters


function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f(input);
						

rest parameters 1

You can create a variable for the remaining items in a list using the syntax ...name


let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
						

rest parameters 2

You can ignore parameters


let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

let [, second, , fourth] = [1, 2, 3, 4];
						

Object destructuring


let o = {
    a: "foo",
    b: 12,
    c: "bar"
}
let {a, b} = o; // This creates new variables a and b from o.a and o.b.
						

Property renaming 1

You can also give different names to properties


let {a: newName1, b: newName2} = o;

// is equivalent to
let newName1 = o.a;
let newName2 = o.b;
						

Property renaming 2

The type of the object needs to be written after the destructuring


let {a, b: newName}: {a: string, b: number} = o;
						

Default values

Default values let you specify a default value in case a property is undefined


function keepWholeObject(wholeObject: {a: string, b?: number}) {
    let {a, b = 1001} = wholeObject;	// b will be 1001 if wholeObject.b is undefined
}
						

Interfaces

TypeScript's type-checking focuses on the shape that values have

This is sometimes called "duck typing" or "structural subtyping"

Without interfaces


function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
// myObj is checked based on its shape
// i.e. it has the same structure that function signature (label property)
printLabel(myObj);
						

With an interface


interface LabelledValue {
    label: string;
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj); // no need for myObj to implement the interface, only its shape matters
						

Optional properties

Interfaces with optional properties are written similar to other interfaces, with each optional property denoted by a ? at the end of the property name in the declaration

Optional properties


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;
}

let mySquare = createSquare({color: "black"});
						

Function types 1

Interfaces are also capable of describing function types


interface SearchFunc {
    (source: string, subString: string): boolean;
}
						
  • Is like a function declaration with only the parameter list and return type given
  • Each parameter in the parameter list requires both name and type

Function types 2

Names of the parameters do not need to match


let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    if (result == -1) {
        return false;
    }
    else {
        return true;
    }
}
						

Function types 3

Names of the parameters do not need to match


let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    if (result == -1) {
        return false;
    }
    else {
        return true;
    }
}
						

Class types

Allows to explicitly enforce that a class meet a particular contract


interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}
						

Extending interfaces

An interface can extend multiple interfaces, creating a combination of all of the interfaces.


interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
						

Classes

Class


class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
						

Inheritance 1


class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}
						

Inheritance 2


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

sam.move();
tom.move(34);

outputs
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
						

public modifier

Members are public by default

private modifier

When a member is marked private, it cannot be accessed from outside of its containing class


class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // Error: 'name' is private;
						

protected modifier

Like the private modifier but protected members can also be accessed by instances of deriving classes


class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
						

Parameter properties

Let you create and initialize a member in one place

Are declared by prefixing a constructor parameter with an accessibility modifier


class Animal {
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
						

Accessors

Intercepting accesses to a member of an object

No getter/setters


class Employee {
    fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}
						

With getter/setter


class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        console.log("fullName changed");
        this._fullName = newName;
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}
						

Static properties


class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
						

Abstract classes

Base classes from which other classes may be derived

They may not be instantiated directly


abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}
						

Abstract classes 1


abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log("Department name: " + this.name);
    }

    abstract printMeeting(): void; // must be implemented in derived classes
}
						

Abstract classes 2


class AccountingDepartment extends Department {

    constructor() {
        super("Accounting and Auditing"); // constructors in derived classes must call super()
    }

    printMeeting(): void {
        console.log("The Accounting Department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}
						

Abstract classes 3


let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type
						

Functions

Named functions


function add(x, y) {
    return x + y;
}
						

Anonymous functions


let myAdd = function(x, y) { return x+y; };
						

Typed functions

Named functions


function add(x: number, y: number): number {
    return x + y;
}
						

Anonymous functions


let myAdd = function(x: number, y: number): number { return x+y; };
						

Full type of the function

It has two parts:

  • the type of the arguments
  • the return type

Uses => to separate parameters and return type


let myAdd: (x: number, y: number)=>number =
    function(x: number, y: number): number { return x + y; };
						

Types can be inferred


// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return  x + y; };

// The parameters 'x' and 'y' have the type number
let myAdd: (baseValue:number, increment:number) => number =
    function(x, y) { return x + y; };
						

Optional and default parameters

Optional parameters 1

In TypeScript, every parameter is required

The number of arguments given to a function has to match the number of parameters the function expects


function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams"); 
						

Optional parameters 2

In JavaScript, every parameter is optional

We can get this functionality in TypeScript by adding a ? to the end of parameters


function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

let result1 = buildName("Bob");                  // works correctly now
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // ah, just right
						

Default parameters


function buildName(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined);       // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result4 = buildName("Bob", "Adams");         // ah, just right
						

Rest parameters

Boundless number of optional parameters


function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
						

Advanced Types

Union types 1

A union type describes a value that can be one of several types

We use the vertical bar (|) to separate each type


/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation
						

Union types 2

We can only access members that are common to all types in the union


interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors
						

Type aliases

Type aliases create a new name for a type

Aliasing doesn’t actually create a new type - it creates a new name to refer to that type


type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === "string") {
        return n;
    }
    else {
        return n();
    }
}
						

String literal types


type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        // ...
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
						

Polymorphic this types

A polymorphic this type represents a type that is the subtype of the containing class or interface


class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
}

let v = new BasicCalculator(2).multiply(5).add(1).currentValue();

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
}

let v = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();
						

Symbols

Smbol is a primitive data type

Symbol values are created by calling the Symbol constructor


let sym1 = Symbol();

let sym2 = Symbol("key"); // optional string key
						

Symbols 2

Symbols are immutable and unique


let sym2 = Symbol("key");
let sym3 = Symbol("key");

sym2 === sym3; // false, symbols are unique
						

Symbols 3

Symbols can be used as keys for object properties


let sym = Symbol();

let obj = {
    [sym]: "value"
};

console.log(obj[sym]); // "value"

// also for function members
const getClassNameSymbol = Symbol();

class C {
    [getClassNameSymbol](){
       return "C";
    }
}

let c = new C();
let className = c[getClassNameSymbol](); // "C"
						

Iterators

Iterables

  • An object is deemed iterable if it has an implementation for the Symbol.iterator property
  • Built-in types like Array, Map, Set, String, Int32Array, Uint32Array have it already implemented
  • Symbol.iterator function on an object is responsible for returning the list of values to iterate on

for..of statements

Lops over an iterable object, invoking the Symbol.iterator property on the object


let someArray = [1, "string", false];

for (let entry of someArray) {
    console.log(entry); // 1, "string", false
}
						

for..of vs for..in statements

for..in returns a list of keys on the object being iterated

for..of returns a list of values of the numeric properties of the object being iterated.


let list = [4, 5, 6];

for (let i in list) {
   console.log(i); // "0", "1", "2",
}

for (let i of list) {
   console.log(i); // "4", "5", "6"
}
						

for..of vs for..in statements

for..in operates on any object; it serves as a way to inspect its properties

for..of is mainly interested in values of iterable objects


let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";

for (let pet in pets) {
   console.log(pet); // "species"
}

for (let pet of pets) {
    console.log(pet); // "Cat", "Dog", "Hamster"
}
						

Modules

Modules

  • Starting with the ECMAScript 2015, JavaScript has a concept of modules
  • They are executed within their own scope
  • Variables, functions, classes, etc. declared in a module are not visible outside the module unless they are explicitly exported
  • Modules are declarative
  • The relationships between modules are specified in terms of imports and exports at the file level.
  • Any file containing a top-level import or export is considered a module.

Loading modules

  • Modules import one another using a module loader
  • At runtime the module loader is responsible for locating and executing all dependencies of a module before executing it
  • CommonJS module loader for Node.js
  • require.js for Web applications.

Export

Exporting a declaration

Any declaration (variable, function, class, type alias, or interface) can be exported

Use export keyword.


export let color = 'red';
export const PI = 3.1415;
						

Single export


// StringValidator.ts
export interface StringValidator {
    isAcceptable(s: string): boolean;
}
						

Multiple exports


// ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
						

export statements


// ZipCodeValidator.ts
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
						

Re-exports


export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// Export original validator but rename it
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
						

Import

Import a single export from a module


import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();
						

Renamed imports


import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
						

Import an entire module


import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
						

Default exports

  • Each module can optionally export a default export
  • there can only be one default export per module

Default export a class declaration


// ZipCodeValidator.ts
export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}

// test.ts
import validator from "./ZipCodeValidator";

let myValidator = new validator();
						

Default export a function declaration


// StaticZipCodeValidator.ts

const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}

// test.ts
import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// Use function validate
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});
						

default export a value


// OneTwoThree.ts
export default "123";

// Log.ts
import num from "./OneTwoThree";

console.log(num); // "123"
						

Exercise 1

Write a function that receives:

  1. number
  2. a boolean or a string
  3. a string or undefined
  4. returns a string

Exercise 2

Write a module that exports a Traveler class and TravelerType enum

The Traveler class has:

  • firstName, middleName, lastName and travelerType
  • A getter for the full name as a string

The enum type contains:

  • Adult, Child

Exercise 3

Write a module that

  • Imports Traveler and TravelerType
  • Creates a traveler intance
  • Write to console the traveler full name

Slides derived from

https://www.typescriptlang.org/docs/

Slides

https://miguelcoba.github.io/typescript-intro

Source code

https://github.com/miguelcoba/typescript-intro-code

Thank you!

Miguel Cobá

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.