TypeScript

Introduction

Example 1 Motivating example

There are (at least) 3 bugs in this code. Can you spot them?

<input id="a" type="number" /> +
<input id="b" type="number" /> =
<span id="result "></span>
<script>
    const aInput = document.getElementById("a");
    const bInput = document.getElementById("b");
    const resultSpan = document.getElementById("result");
    const computeResult = () =>
        (resultSpan.value = aInput.value + bInput.value);
    aInput.addEventListener("input", computeResult);
    bInput.addEventListener("input", computeResult);
    computeResult();
</script>

Source

What is a type?

A type system is a syntaxic method for automatically checking the absence of certain erroneous behaviors by classifying program phrases according to the kinds of values they compute.

Benjamin C. Pierce, Types and Programming Languages

Why do we need types?

  • Early bugs discovery
  • Documentation
  • Autocompletion
  • (Performance)

Variable type annotations

Typescript (example 2):

const foo: number = 42;

Python (example 3):

foo: int = 42

C:

int foo = 42;

Everyday types

Reference

TypeScript Handbook > Everyday types

Primitive types

const foo: number = 42;
const bar: string = "Hello world";
const baz: boolean = true;
const qux: undefined = undefined;
const quux: null = null;

Array types

const foo: number[] = [42, 451, 1984];
const bar: string[] = ["Hello Bradbery", "Hello Orwell"];
const baz: boolean[] = [false, false, true];

Parameter type annotations

function greet(name: string) {
    console.log("Hello " + name);
}
greet("Ada");

Return type annotations

function greet(name: string): string {
    return "Hello " + name;
}
console.log(greet("Ada"));

function add(a: number, b: number): number {
    return a + b;
}
console.log(add(40, 2));

Object types

const center: { x: number; y: number } = { x: 0, y: 0 };

// The parameter's type annotation is an object type
function printCoord(point: { x: number; y: number }) {
    console.log("The coordinate's x value is " + point.x);
    console.log("The coordinate's y value is " + point.y);
}

printCoord(center);

Union types

const foo: number | string = 42;
const bar: number | string = "";
const baz: number | undefined = 42;
const qux: number | undefined = undefined;

Type aliases

type Human = { firstname: string; lastname: string };
type Robot = { serialNumber: number };

function printName(member: Human | Robot) {
    // ...
}

Interfaces

interface Human {
    firstname: string;
    lastname: string;
}

interface Robot {
    serialNumber: number;
}

Interface extension

interface Human {
    firstname: string;
    lastname: string;
}

interface Developer extends Human {
    githubRepo: string;
}

Narrowing

Reference

TypeScript Handbook > Narrowing

Equality narrowing

To differentiate one value (like null or undefined) from other values, we can use an equality check.

function getLength(arg: string | undefined): number {
    if (arg === undefined) {
        return 0;
    } else {
        return arg.length;
    }
}

typeof type guards

To differentiate a primitive type from another, we can use the typeof keyword that returns the type of a variable as a string. This only works for primitive types (string, number, boolean, undefined or null).

function sinOrLength(arg: number | string): number {
    if (typeof arg === "number") {
        return Math.sin(arg);
    } else {
        return arg.length;
    }
}

Discriminated unions

Used to differentiate between interface types.

interface Human {
    type: "human";
    firstname: string;
    lastname: string;
}
interface Robot {
    type: "robot";
    serialNumber: number;
}

function printName(member: Human | Robot) {
    if (member.type === "robot") {
        console.log(member.serialNumber);
    } else {
        console.log(member.firstname + " " + member.lastname);
    }
}

instanceof narrowing

Used to differentiate between class types.

class Human {
    firstname: string;
    lastname: string;
    // constructor ...
}
class Robot {
    serialNumber: number;
    // constructor ...
}
function printName(member: Human | Robot) {
    if (member instanceof Robot) {
        console.log(member.serialNumber);
    } else {
        console.log(member.firstname + " " + member.lastname);
    }
}

How to use TypeScript

Compilation

TypeScript files cannot be directly understood by a web browser. We need to compile them to JavaScript first.

In order to do so, we will use Node.js, NPM, Webpack and the TypeScript compiler.

Node.js

Node.js allows to run JavaScript outside of the browser.

We will it use for two purpose:

  1. Now: to run build tools which are written in JavaScript,
  2. Later: to run a web server written in JavaScript.

NPM Project

The Node Package Manager (NPM) is used to manage the dependencies of a project.

Each NPM project contains a package.json file that declares the dependencies of the project and other metadata.

Our package.json file

{
    "name": "[your-exercise-name]",
    "version": "1.0.0",
    "author": "[your-name]",
    "private": true,
    "scripts": {
        "build": "webpack",
        "build:watch": "webpack --watch"
    },
    "devDependencies": {
        "ts-loader": "^9.2.3",
        "typescript": "^4.3.2",
        "webpack": "^5.38.1",
        "webpack-cli": "^4.7.0"
    }
}

Webpack

Webpack is the build tool we will use. It can convert files from one language to another and bundle them together. We will use it to convert all TypeScript files from the src directory to a single JavaScript file dist/bundle.js.

Webpack is configured by a webpack.config.js file.

Our webpack.config.js file

const path = require("path");

module.exports = {
    entry: "./src/main.ts",
    mode: "development",
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: [".tsx", ".ts", ".js"],
    },
    output: {
        filename: "bundle.js",
        path: path.resolve(__dirname, "dist"),
    },
};

TypeScript config

The TypeScript compiler itself also can be configured, using a tsconfig.json file.

We will use the following:

{
    "compilerOptions": {
        "target": "esnext",
        "module": "esnext",
        "strict": true
    }
}

Example 4 Addition

Exercise 1 Celcius-Fahrenheit converter

Goal

Reproduce the degrees converter from Google search:

Step 1 Celsius-Fahrenheit conversion

The page should contain two <input>: one with the value in Celsius degrees and the second in Fahrenheit degrees.

Changing the value of one input should automatically update the value of the other.

Make sure that this also work if an input is empty.

Step 2 (extra) Unit choosers

Under each input, there should be a <select> element allowing to change the degrees unit (“Fahrenheit”, “Celsius” or “Kelvin”), as in the Google widget.

Data and view separation

Risks with manual DOM manipulation

  • It makes the code hard to reason about.
  • It makes it easy to introduce bugs, especially mismatch between data and user interface.
  • It is not obvious to choose which elements should be static HTML or dynamically generated with JavaScript.

Solution

Use data as a single source of truth, and derive the view from it.

The view should never update itself directly.

Diagram

Example 5 Numbers sum

Exercise 2 To-do list

Setup

  • Copy the content of 02-typescript/examples/05-array-sum example, or from your first exercise, to a folder 02-typescript/02-to-do-list in your exercises repository (so that you have all the configuration files ready).
  • Then, run npm install.
  • Remove all content of the src/main.ts file.
  • To compile your TypeScript code, use the npm run build (to compile once) or npm run build:watch (to automatically recompile on every change).

Step 1 State type

  • Define a State type representing the state of your application. It should be an array of objects, each with an attribute done and an attribute title.
  • Define an initial example state in a state variable of type State.

Step 2 render function

Write a render function that renders the state to the DOM.

Step 3 Add item

Add a way to add a new item to the to-do list.

Step 4 Add checkboxes

Display a checkbox for each item showing if the item is done or not. Clicking on the checkbox should change the value of the corresponding done attribute.

Step 5 (extra) Make the items sorted

The to-do items should always be sorted alphabetically.

Step 6 (extra) Store state in local storage.

Each time the state changes, save it to local storage so that it can be restored on next page load.

Step 7 (extra) Add a filter feature

Add a way to show only items that are done or not done.

Classes (extra)

Classes

Remember ES6 classes?

class Human {
    constructor(firstname, lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
}

With type annotations:

class Human {
    firstname: string;
    lastname: string;
    constructor(firstname: string, lastname: string) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
}

Interfaces vs Classes

Here, ada is a POJO (Plain Old JavaScript Object), created using the object literal notation ({ ... }):

interface Human {
    firstname: string;
    lastname: string;
}
const ada: Human = {
    firstname: "Ada",
    lastname: "Lovelace",
};

Interfaces vs Classes (cont’d)

Here ada is an instance of the Human class, constructed with the new keyword:

class Human {
    firstname: string;
    lastname: string;
    constructor(firstname: string, lastname: string) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
}
const ada: Human = new Human("Ada", "Lovelace");

Interfaces vs Classes (cont’d)

The main added value of a class is the possibility to add methods:

class Human {
    firstname: string;
    lastname: string;
    constructor(firstname: string, lastname: string) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
    toString(): string {
        return this.firstname + " " + this.lastname;
    }
}
const ada: Human = new Human("Ada", "Lovelace");
ada.toString(); // "Ada Lovelace"

Exercise 3 The MyHTMLElement class

Starting point

class MyHTMLElement {
    tagName: string;

    constructor(tagName: string) {
        this.tagName = tagName;
    }

    appendChild(child: MyHTMLElement) {}
}

Copy this code in a file 02-typescript/01-my-html-element.ts in your repository.

Step 1 Add a children attribute

Add a children attribute to the MyHTMLElement class so that the following code type-checks (for now, this means that nothing should be underlined in red in VSCode):

const element: MyHTMLElement = new MyHTMLElement("div", []);
element.children[0].tagName;

Question: in the real HTMLElement type, is the children attribute an array?

Step 2 Add an insertBefore method

Write an insertBefore method declaration (without implementation) so that the following code type-checks:

const child: MyHTMLElement = new MyHTMLElement("span", []);
const element: MyHTMLElement = new MyHTMLElement("div", []);
element.insertBefore(child, new MyHTMLElement("span", []));

Step 3 Add a querySelector method

Write a querySelector method declaration that takes a string as an argument and returns either a MyHTMLElement or null. Use return null; as implementation for now.

Step 4 (extra) Add a classList attribute

Add a classList attribute so that the following code type-checks:

element.classList.add("a-class");
element.classList.has("a-class");
element.classList.remove("a-class");

Step 5 (extra) Implementation

Write the methods’ implementations.