Introduction to TypeScript

Introduction to TypeScript image

TypeScript, as its name suggests, is a typed superset of JavaScript, which means that all JavaScript code is valid in TypeScript – making it very easy to get started. In addition, TypeScript has several language features including static typing, most ES6 syntax, and object-oriented programming constructs. It is a compiled language, which means you’ll have to transpile it to JavaScript before it can be run, but the benefits are well worth it.

Many large-scale projects are adopting TypeScript for its maintainability, such as Angular 2, Asana, and more. TypeScript’s optional type annotations and features make it extremely easy to migrate a project from JavaScript part by part. It’s also very simple to start a project in pure TypeScript, which we will be covering in this tutorial.

Prerequisites

A working knowledge of JavaScript, object-oriented programming, and the command line is assumed. The only prerequisites are a code editor and npm, the Node.js package manager.

If you do not have npm installed, install the appropriate Node.js runtime for your operating system. Node.js is a JavaScript runtime outside the browser (for use in servers, etc), and npm is a package manager for various JavaScript packages and command line tools.

Alternatively, you can test out TypeScript and follow this tutorial without installing anything by using the TypeScript online playground.

1. Install Typescript

The TypeScript compiler is available as an npm package; you can install it by running:

npm install -g typescript

Once the above step is completed, the TypeScript compiler tsc will be available as a command line tool. We’ll demonstrate how to use this below.

2. Building a simple app

We will build a simple library application to demonstrate core TypeScript syntax and features including types, classes, and various ES6 features (that get transpiled down to ES5, which is supported in most major browsers).

Quick background

To start, make a directory that will hold all of our files, and create this bare-bones HTML file, index.html, that will run the JavaScript code.

<title>Intro to Typescript</title>
<script src="intro.js"></script>

Note that the included script is just a normal .js file! We will transpile our TypeScript code into plain JavaScript. Next, in the same directory, create the intro.ts file:

function hello(name: string) {
    return "Hello, " + name;
}

document.body.innerHTML = hello("world!");

This is standard JavaScript with the exception of name: string on the first line. This is called a type annotation, which indicates that the name parameter must be a string.

Next, to compile, navigate to the directory in your terminal and run  tsc intro.ts, which will transpile intro.ts into JavaScript. There should be no output from this command, but an intro.js file will be silently generated. Let’s take a look at the generated code:

function hello(name) {
    return "Hello, " + name;
}

document.body.innerHTML = hello("world!");

This is exactly the same code as above, except without the type annotation! So, this means that type checking is entirely handled by the compiler, which will print an error if there is a type mismatch, e.g. hello(1), but the outputted JavaScript has no knowledge of types. This is consistent with how compilation works for statically typed languages such as C.[1]

If you open index.html in a browser, the page should display Hello, world!, as expected.

Building the app

We will build a library application using object-oriented constructs in TypeScript. Books are an essential part of any library, so we can start out by creating a Book class:

class Book {
    title: string;
    isbn: number;  // Unique number used to identify books
    constructor(title: string, isbn: number) {
        this.title = title;
        this.isbn = isbn;
    }
}

This is a departure from standard JavaScript syntax – let’s dig into what’s happening here. We define a class called Book, with the two members (also called instance variables) title and isbn. Both of these variables are annotated with their types. We also create a constructor for this class, which will instantiate a Book object with the two parameters.

For simplicity, we won’t discuss the generated JavaScript anymore, but feel free to take a look yourself.[2]

A much more compact, and stylistically better version of the above is:

class Book {
    constructor(public title: string, public isbn: number) {}
}

The public prefix to the constructor parameters tells the compiler to automatically declare and set the title and isbn members – it is equivalent to the previous code.

In addition to a title and ISBN, a library book may have an additional property that tracks whether it is available or not. Let’s implement this in a subclass of Book called LibraryBook. In addition, whenever the availability of a book changes, we’ll want to print an update. This can be done with private members and getter/setter functions in TypeScript:

class LibraryBook extends Book {
    private _available = true;
    constructor(title: string, isbn: number) {
        super(title, isbn);
    }

    get available() { return this._available; }
    set available(isAvailable: boolean) {
        console.log(`"${this.title}" is now ${isAvailable ? 'available' : 'unavailable'}`);
        this._available = isAvailable;
    }
}

There are many interesting things happening here! Let’s go through and discuss line-by-line. The first line declares a new class LibraryBook, that extends our previous class Book. This means that LibraryBook will inherit the structure of a Book, i.e. the two members title and isbn.

Next, we define the private member_available to be true on initialization, which is similar to how we explicitly declared the public member title previously, but with the addition of the private keyword. A private member can only be accessed within its own class, and not externally.[3] This is necessary because we don’t want _available to be set directly, so we can print an update before its value changes; you’ll see how in a bit. The underscore prefix is a naming convention for private members.

The next function defines the constructor for LibraryBook. The super() call in its body will simply run the constructor of the parent class Book there, which sets the two members title and isbn.

Next, we have two functions defined with the keywords get and set – these are called accessors, or getters/setters. These functions are similar to creating a public member called available, except the functions are called whenever we read or write to the member. In other words, a statement like var x = book.available (get/read) or book.available = false (set/write), will call the appropriate function, even though the syntax suggests a simple variable expression.

The getter function is fairly straightforward. The setter function will set book._available to the argument isAvailable, taken from a statement such as book.available = isAvailable. It also prints an update using an ES6 template string.

Now we understand how LibraryBook is implemented! As a side note, we’ve now seen the data types string, number, and boolean. There are a few other types in TypeScript, including an any type, which allows a variable to take on any value, regardless of type – this is occasionally useful when working with existing JavaScript code.

Finally, let’s create a Library class that keeps track of a collection of LibraryBook‘s.

class Library {
    books: LibraryBook[] = [];
    addBooks(...newBooks: LibraryBook[]) { 
        this.books.push(...newBooks); 
    }
    checkOut(book: LibraryBook) { 
        book.available = false; 
    }
    checkIn(book: LibraryBook) { 
        book.available = true; 
    }
    printBooks() {
        for (var book of this.books) {
            let {title, isbn} = book;
            console.log(`Title: "${title}", ISBN: ${isbn}`);
        }
    }
}

This is fairly straightforward, but there is some important syntax hidden in these short functions, so let’s also go through this class line-by-line. At the top, we declare an independent class Library, which does not inherit from any other class.

Next, we define the sole member of Library, called books, initialized to an empty array. Note the array type annotation syntax is : LibraryBook[], which means an array of LibraryBook‘s. Also, this class does not have an explicit constructor, since there are no other members to set!

The addBooks() function uses an ES6 rest parameter to take in an arbitrary number of arguments, which we then append to the books member by using the ES6 destructuring syntax. Note an idiosyncrasy in using rest parameters in TypeScript: the argument is an array type : LibraryBook[] rather than an element type : LibraryBook.

The checkOut() and checkIn() functions are fairly straightforward. They take advantage of the getter/setter syntactic sugar we set up beforehand in the LibraryBook class.

The printBooks() function will log every book currently in the library. We use an ES6 for…of loop to iterate through our books member. For each LibraryBook object, we use ES6 object destructuring to set the block-scope variablestitle and isbn to the appropriate values in the LibraryBook instance. We can destructure a TypeScript object! Last, we print the book information to the console.

Finally, our app is complete! Try it out yourself – replace the contents of intro.ts with the following code:

class Book {
    constructor(public title: string, public isbn: number) {}
}

class LibraryBook extends Book {
    private _available = true;
    constructor(title: string, isbn: number) {
        super(title, isbn);
    }

    get available() { return this._available; }
    set available(isAvailable: boolean) {
        console.log(`"${this.title}" is now ${isAvailable ? 'available' : 'unavailable'}`);
        this._available = isAvailable;
    }
}

class Library {
    books: LibraryBook[] = [];
    addBooks(...newBooks: LibraryBook[]) { 
        this.books.push(...newBooks); 
    }
    checkOut(book: LibraryBook) { 
        book.available = false; 
    }
    checkIn(book: LibraryBook) { 
        book.available = true; 
    }
    printBooks() {
        for (var book of this.books) {
            let {title, isbn} = book;
            console.log(`Title: "${title}", ISBN: ${isbn}`);
        }
    }
}

var bookA = new LibraryBook('Gödel, Escher, Bach: an Eternal Golden Braid', 9780465026562);
var bookB = new LibraryBook('Structure and Interpretation of Computer Programs', 9780262510875);
var library = new Library();
library.addBooks(bookA, bookB);
library.printBooks();
library.checkOut(bookA);
library.checkIn(bookA);

At the bottom, we include some code to test out our library app. Next, compile the code with, tsc intro.ts --target ES5 (we specify the output to be ES5 since we’ve used ES6 features), and run index.html in a browser. Make sure to open the JavaScript console! You should see the following output:

Introduction to TypeScript - Console Output

Success! Definitely play around with creating new books, adding them to the library, and checking them out in the console – you’ll see that our app functions normally in JavaScript.

For bonus points, see what happens when you have a type error in your code, for example, if you accidentally swapped the two arguments for the LibraryBook constructor.[4]

3. Summary

We’ve covered a lot in this tutorial! Through building a application to track books in a library, you learned about core TypeScript features including type annotations, classes, inheritance, public/private members, accessors, and a number of ES6 features that are supported in the language. An important TypeScript concept to remember is that almost all of these benefits and features apply only at compile time, and not at runtime.

Definitely play around with the above code if you haven’t already, also available on GitHub. It’s only 35 lines, but is packed with quite a few important details. There are many wonderful TypeScript features that we weren’t able to cover in this tutorial – if you’re eager to learn more, go ahead and check out their documentation.



  1. For example, there is no notion of data types at the assembly language level, so type information disappears after C code is compiled. This analogy is not perfect, but shows that we be reasonably type safe at runtime with this model. ↩︎

  2. If you looked at the generated JavaScript for a TypeScript class, you may recognize that this is implemented with an Immediately-Invoked Function Expression to manage scope and state. ↩︎

  3. An important caveat of private members is that their access restriction only holds at compile time, since JavaScript cannot enforce it at runtime, but this is sufficient for architectural purposes. See the documentation for more details. ↩︎

  4. If you compile 'intro.ts' with a statement like 'new LibraryBook(4321, 'Title')', the compiler will print the error 'Argument of type 'number' is not assignable to parameter of type 'string''. However, the JavaScript is still generated by default ↩︎

KEEP MOVING FORWARD

Kenny Song / javascript