Introduction to TypeScript

TypeScript is a superset of JavaScript ES6. Its main contribution is to make ES6 strongly typed. The compiler can catch many type related problems. Strong typing also allows an editor to offer code completion, refactoring and code navigation.

In this tutorial, we will get introduced to the type system of TypeScript.

Before you start working on this tutorial, please install the required software as mentioned in the Set Up Guide here.

Part 1 -  Setup

For this tutorial, we will create a new directory and install TypeScript.

1.Create a folder 'C:\LabWork' on your machine. Within the C:\LabWork directory create a subdirectory named 'tslab' the result should be the following:

C:\LabWork\tslab

2. Open a command prompt window and navigate into the 'tslab' directory.

3. Execute the following to create a new package.json file. The command will prompt you for more information. Take all the default values and proceed until the command is complete:

npm init

4. Install TypeScript into the local project directory ('tslab') by executing the following command:

npm install typescript@3.1.1 --save-dev

You can ignore any WARN messages.

5. Copy a tsconfig.json file from here into the C:\LabWork\tslab directory. This file configures the TypeScript compiler and must be present in the folder where the compiler is run from.

6. Edit the package.json file. Add the line shown in bold below to the scripts section of the file:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "tsc": "./node_modules/.bin/tsc"
  },

 7. Save the file.

 8. Try running tsc from inside the 'tslab' directory using the following command:

npm run tsc

9. You should get the following error indicating that no source files were found. We will be creating a source file in the next section:

error TS18003: No inputs were found in config file...

Note: In this lab, we are calling the TypeScript compiler directly from the command prompt. In other labs, the same compilation step is executed behind the scenes by the Angular build system.

Part 2 - Variable Types

1. Within the C:\LabWork\tslab directory create a file called play.ts. Make sure the file does not have an extension like '.txt' automatically added to it.

2. In this file, add these lines to define and print a numerical variable.

var age: number = 20
console.log(age)

The ":number" bit comes from TypeScript. The rest is basic JavaScript.

3. Save changes.

4. From the command prompt go to the C:\LabWork\tslab directory. Then run this command to compile the code.

npm run tsc

If all goes well the command you should see the following output:

C:\LabWork\tslab>npm run tsc

> tslab@1.0.0 tsc C:\LabWork\tslab
> tsc

5. The above command will transpile the play.ts file and place the result in the ./out directory. Do a directory listing to confirm this:

C:\LabWork\tslab>dir /b out
play.js
play.js.map

6. Enter the following command to run the code:

node out/play.js

The output should look like this:

C:\LabWork\tslab>node out/play.js
20

7. Now we will deliberately introduce a type error. In play.ts initialize the age variable like this.

var age: number = "Too old"

8. Save play.ts.

9. Transpile the code again using the tsc command:

npm run tsc

This time the results will be an error like this:

C:\LabWork\tslab>npm run tsc
play.ts(2,5): error TS2322: Type '"too old"' is not assignable to type 'number'.

This error illustrates type safety. It shows that we have incorrectly assigned a string type to a number variable.

Part 3 - Function Argument Types

1. Delete or comment out all lines in play.ts.

2. Add a function like this.

function printPerson(name:string, age:number) {
  console.log(`Name: ${name} age: ${age}`)
}
printPerson("Billy", 8)

Watch out for the string with back ticks. It is a template string where you can insert variables using ${variable} syntax.

3. Save changes.

4. Compile the code.

npm run tsc

5. Run the file using the following command:

node out/play.js

The output should look like this:

C:\LabWork\tslab>node out/play.js
Name: Billy age: 8

6. Edit play.ts and add a call to the "printPerson" function using incorrect parameter types. For example, the following call reverses the order of parameters:

printPerson(8, "Billy")

7. Run the compiler again and make sure that you see an error message.

Part 4 - Class and Inheritance

We will learn about class and inheritance in this section. We will model various products sold by a travel company like tours, shows, and dining.

1. Delete or comment out all lines in play.ts.

2. Add a class like this.

class Product {
  title: string;
  price: number;
  id: number;

  constructor(id: number) {
    this.id = id
  }

  printDetails() {
    console.log(`Title: ${this.title}`)
    console.log(`ID: ${this.id}`)
    console.log(`Price: ${this.price}`)
 } }

3. Add another class that inherits from Product.

class Tour extends Product {
  duration: string;

  constructor(id: number, duration: string) {
    super(id);

    this.duration = duration
  }

  printDetails() {
    super.printDetails()
		
    console.log(`Duration: ${this.duration}`)
  }
}

4. Write a function named 'test' that takes a Product as an argument. Note: This is a standard function and not part of the classes you just created. Make sure the 'test' function is not inside the brackets of either of the classes you just created.

function test(p: Product) {
  p.printDetails()
}

5. Next, create an instance of Tour and call the 'test' method. This code should also be on its own outside of any class definition.

var t = new Tour(1, "8 hours")
t.title = "Trip to the Taj Mahal"
t.price = 1200.00
test(t)

The key thing to observe here is that even though test() takes as an argument a Product, it will actually invoke the printDetails() method of the Tour class.

Also, you may notice that the lines of code above do not end with a semicolon. This is perfectly alright. In these cases, JavaScript uses the carriage return at the end of the line to determines the end of the command.

6. Save changes

7. Compile the code.

8. Run the code:

node out/play.js

The output should look like this:

C:\LabWork\tslab>node out/play.js
Title: Trip to the Taj Mahal
ID: 1
Price: 1200
Duration: 8 hours

9. You can do this optional step all on your own. Add a class called Dining that extends Product and has these fields:

  • cuisine of type string.

  • childPrice of type number.

Create the constructor and a reasonable implementation of the 'printDetails' method.

Create an instance of it and pass it to the test() function.

This ability for a Tour object to appear as Product is called polymorphism. Inheritance is one of the ways to achieve polymorphism. The other being interface. We will get into that next.

Part 5 - Interface

An interface in TS describes the shape of an object. An interface lists a set of variables and methods. An object or class claiming to conform to that interface must declare those variables and methods.

In our ongoing example, we have received a few new requirements.

  • Products like Tour and Dining are bookable. We need to store a list of available dates for them. During booking a user will select a date.

  • A Tour is cancelable and there is a cancelation fee associated with it. There may be other cancelable products in the future.

We realize that there may be similar requirements coming in the future. These features (bookable, cancelable etc.) can be associated with products in an independent manner. This makes it hard to model them using inheritance alone (TypeScript allows inheriting from a single class only). Interface is the right approach here.

1. Create an interface called Bookable like this.

interface Bookable {
    availableDates: 
}

2. Create an interface called Cancelable like this.

interface Cancelable {
    cancelationFee: number
}

3. Make the Tour class implement these two interfaces. This is shown in bold below.

class Tour extends Product implements Bookable, Cancelable {
...

4. Add these two fields to the Tour class.

availableDates: 
cancelationFee: number

5. Save play.ts.

6. Compile the code and make sure there are no errors.

7. Add the following as a global method (one that is outside of any classes at the global level) that cancels a booking:

function cancelBooking(c: Cancelable) {
  console.log("Canceling booking. Charges: %d", c.cancelationFee)
}

8. Add these lines of code in bold at the end of the file to test the cancelBooking() function.

var t = new Tour(1, "8 hours")

t.title = "Trip to the Taj Mahal"
t.price = 1200.00
t.cancelationFee = 100.00
cancelBooking(t)

9. Save. Let the file compile. Then run. The output should be:

C:\LabWork\tslab>node out/play.js
Canceling booking. Charges: 100
Title: Trip to the Taj Mahal
ID: 1
Price: 1200
Duration: 8 hours

A Tour class object can now appear as a Product, a bookable item, and a cancelable item. The last two polymorphic behaviors are achieved using interfaces.

Part 6 - Generics

Currently, our cancelBooking() method takes as input a Cancelable object. Unfortunately, we do not have access to the original price of the product or the product ID. Without knowing that we can not properly issue a refund. The correct data type should be a Product that is also Cancelable. How do we model that? Generics constraints will come to the rescue.

1. Make a copy of the cancelBooking function and call it cancelBooking2

2. Change the signature of cancelBooking2() as shown in bold face below.

function cancelBooking2<T extends Cancelable & Product>(c: T) {

Here T is a generic place holder for a type. As per our constraints, T must extend Product and implement Cancelable.

3. Modify the body of the cancelBooking2 function as shown below.

function cancelBooking2<T extends Cancelable & Product>(c: T) {
  console.log("Canceling: %s (%d)", c.title, c.id)
  console.log("Price: %d", c.price)
  console.log("Cancelation fee: %d", c.cancelationFee)
  console.log("Total refund: %d", c.price - c.cancelationFee)
}

4. Add the following call to cancelBooking2 at the end of the file and comment the previous call:

//cancelBooking(t);
cancelBooking2(t);

5. Delete or comment the call to test.

//test(t)

6. Save, compile and run. You should see this output.

Canceling: Trip to the Taj Mahal (1)
Price: 1200
Cancelation fee: 100
Total refund: 1100

Generics let you work with completely unknown data types from a method or class. Optionally, as we did here, you can put certain constraints to that type. The end result is a mix of flexibility and type safety.

Part 7 - Implementing an Interface from an Object

Previously we have seen how a class implements an interface. In TS an object can also conform to an interface. This comes in handy for type safe programming.

Consider a typical JavaScript scenario where you supply several configuration options in an object.

configSomething({
    directory: "/foo",
    file: "bar.txt",
    maxSize: 1024
});

JavaScript cannot enforce any kind of structure to this object. In TS we can specify that this object must conform to an interface.

1. Let's say that in the example above, directory and file are mandatory fields. And maxSize is optional. In play.ts add an interface like this to model that.

interface ConfigOption {
    directory: string
    file: string
    maxSize?: number
}

That ? after maxSize makes it an optional.

2. Add a global method like this.

function configSomething(op: ConfigOption) {
    op.maxSize = op.maxSize || 1024

    console.log("Directory: %s", op.directory)
    console.log("File: %s", op.file)
    console.log("Max size: %s", op.maxSize)
}

3. Call that method like this.

configSomething({
    directory: "/dir1",
    file: "persons.json"
})

4. Comment the call to 'cancelBooking2'.

5. Save and let the file compile.

6. Run the code.

node out/play.js

7. The output should be:

Directory: /dir1
File: persons.json
Max size: 1024

8. Now introduce an error by changing the property name "file" to "path" as shown in bold below:

configSomething({
    directory: "/dir1",
    path: "persons.json"
})

9. Save and let compile. You should get a compilation error like this:

... error TS2345: Argument of type '{ directory: string; path: string; }' is not assignable to parameter of type 'ConfigOption'.
  Object literal may only specify known properties, and 'path' does not exist in type 'ConfigOption'

10. Fix the problem, save and compile.

11. Close all open files and command prompts.

Part 8 - Review

In this lab, we took a look at some commonly used features of TypeScript.

In summary:

  • Variables and function parameters can have strong typing.

  • You can develop classes and model inheritance between them.

  • A class can inherit from only one class but implement many interfaces.

  • Both inheritance and interface implementation lead to polymorphism.

  • Generics are a powerful way to write classes and methods without knowing the exact nature of the types. This is mostly useful for framework and library developers who may not anticipate all possible scenarios in which their code may be used. Optionally, you can define constraints for added type safety.

Karandeep Kaur June 25, 2019

Leave a Reply

Your email address will not be published. Required fields are marked *

Web age solutions blog Zones