lilt: Lightweight Interactive Learning Tool

Topic 1

Introduction to TypeScript

JavaScript and typing

JavaScript is well-known as the leading language of the Web. It has been used on the client-side more or less ever since the Web was first invented, and increasingly in recent times has found use on the server too, due to the emergence of Node.js as one of the leading technologies for server-side web development.

JavaScript nowadays is a powerful and full-featured language, with which you can develop full-featured and complex applications. Tt is loosely-typed. When you declare a variable, you do not declare the data type of that variable, and you can subsequently store data of a different type in the same variable. Nonetheless variables at a given moment do have an implicit type, based on what data you store in the variable.

JavaScript has several built-in data types, which include:

  • number : a number (can be either integer or decimal);
  • boolean : a boolean (true or false);
  • string : a string;
  • null : the data type of a null variable. null is a special data type and value which is used to explicitly indicate that the variable doesn't have a valid value.
  • undefined : the contents of the variable have not been defined yet. Any variable which is declared but not initialised will be undefined.

So in JavaScript, code like this is legal:

Note how the variable a is initialised to the value 1. This will set the type of variable a to number. We then reset a to a string. a then becomes a variable of type string. You can obtain the type of a variable using the typeof operator, for example:

This will produce the following output:

So what is the problem with this? The issue is that it can introduce bugs to our code quite easily. By allowing variables to contain any data type, we can inadvertently introduce bugs to our code by setting a variable to a data type we do not expect, including null and undefined (a common source of crashes).

Introducing TypeScript

To address this problem, the TypeScript language (see website here) has been gaining popularity in recent years - indeed so much so, that it is now the number one language on GitHub. TypeScript adopts a typing system similar to Kotlin, in the sense that you can either declare the type explicitly, or the type is assigned implicitly based on the data you store in a variable.

However, unlike JavaScript, once a variable contains data of a specified, or inferred, type, you cannot change the type of data held in the variable to something else. Thus, these kinds of bugs cannot happen.

Hello World in TypeScript

Here is a potential "Hello World" program in TypeScript. TypeScript files are saved with the extension .ts, so this could be hello.ts:

Note how we create a variable called hello and assign its data type explicitly as string. This means that we cannot then change the type of data held in the variable, so the following would be illegal, unlike JavaScript:

This would be illegal because hello has been declared as a string variable, and thus cannot hold any other type of data.

How do we run TypeScript?

If you try to run the above program directly in Node.js, it will not work. Node.js does not understand TypeScript directly. Instead, you have to do one of two things:

  1. Compile the TypeScript into JavaScript, and then run that. To do this, we use the TypeScript type-checker and compiler, tsc (see here). This also checks the TypeScript code (performing type-checking) to ensure that the syntax of TypeScript is not violated.

  2. Use tsc only for type-checking, and then use a separate tool to actually execute the TypeScript directly. There are various tools that will do this; we will use the tsx tool (see here), an enhanced version of Node.js which can interpret TypeScript. This has the partial disadvantage that the TypeScript is interpreted twice (once for the type-checking, and once on execution) but is also somewhat cleaner because intermediate JavaScript files are not generated which can clutter up a project.

Running tsc

The syntax to run tsc is simply tsc filename, for example:

By default, a JavaScript output file (.js) is produced, which can then be run by node. However, with the --noEmit option, you can tell tsc to type-check your code without producing an output .js file:

Having done the type-checking (there is no point writing TypeScript rather than JavaScript if you're not going to type-check it!) you can then run it directly with tsx:

Types available in TypeScript

The JavaScript types detailed above, such as string and number, are all available in TypeScript.

Implicit types

As in Kotlin, the type of a variable is inferred if it is not stated specifically. For example:

Function parameters and return values

In TypeScript, we specify the types of function parameters and return values. For example:

We could then call it as follows:

Because we have specified the types of the parameters, it would be an error to specify any other data type. So this would be an error:

We can also specify the return types, for example:

Here we are specifying that the cube() function returns a value of type number.

Multiple types

We can use the or symbol, |, to specify that a variable may be of multiple types. Here is an example of the previous printText() function which can accept strings or numbers:

Defining our own types

We can define our own types in TypeScript. One example of this is JavaScript classes, but we can also define what are called interfaces. An interface is a specification of properties and methods that the type must contain. In that respect it's similar to Kotlin interfaces, though TypeScript interfaces often contain just properties, whereas in Kotlin they normally contain methods. For example we could define an interface representing a Person:

We could then pass a Person into a function as a parameter:

Note here that any object with name and age properties would be accepted as a parameter to printPerson(). It does not have to be explicitly declared as a Person object. So something like this would work:

Any data type which contains name and age properties would be accepted. So for example we could define an Employee class as follows:

Note how the Employee class contains name and age properties, of type string and number respectively. This means it conforms to the Person interface - unlike Kotlin, we do not have to state this explicitly - and thus will be accepted as a parameter to printPerson():

Exercise 1

Look at this code.

  • What do you think would happen if we try to compile / type-check it with tsc?

Answer to exercise 1

In this code, we have explicitly declared the variable tim as type Person. We then set tim equal to a new Employee object (which is allowed as Employee conforms to the Person interface) but we also try to print out the jobTitle of tim. Even though tim is equal to an Employee object, this is not allowed as the declared type of tim is Person, thus only properties of Person can be accessed. tsc sees tim as a Person and checks that only Person properties are being used.

Type casting

However, in cases where we know that an object of a less specific type actually refers to an object of a more-specific type, we can perform type casting to tell tsc that in this case, the object is of the more specific type. Just like Kotlin, we use the as keyword to perform type casting. So this would work.

or, we can typecast on the fly like this:

Strict mode

By default, tsc runs in a somewhat less-strict mode. For example, null-checking is not strictly enforced by default. This TypeScript will compile by default:

However, the value of TypeScript is better realised when more strict type-checking is enabled, including null checks. A common source of errors in software development is unexpected null values, as we have seen when looking at Kotlin's null-safety features. Luckily we can enforce strict mode easily with the --strict compiler option:

With the --strict option, the above code will fail to compile due to null being passed to greet() where a string is expected.

You can see the full list of strict checks here.

Exercise 2

Look at this code.

  • How would you change this code so that it compiles?

Note in this example the ?? operator. This is known as the nullish-coalescing operator and allows us to specify a default value if a variable is null or undefined.

Answer to exercise 2

As we want to be able to pass null in for the greeting, we should change the type of the greeting parameter to string | null.

The tsconfig.json file

So far, we have controlled tsc's behaviour with compiler options, for example --noEmit to typecheck only, and --strict to enforce strict mode. There are many different options which can be passed into tsc (for a list, see here), and if we were to pass them all via the command-line, it might start becoming a bit long-winded. So instead, we can control tsc's behaviour by means of the configuration file tsconfig.json. This allows us to set options in a JSON file: once this has been written we can then simply use tsc (with no parameters) and the options in tsconfig.json will be applied.

Here is an example of a tsconfig.json:

Note the compilerOptions property within the JSON. This is a series of key/value pairs which define the various options: here, we have set strict and noEmit.

Also note the files property. This is an array of files to be compiled/type-checked by tsc. By default, all .ts files in the current directory will be processed by tsc but we can restrict this to the files specified in the files array.

Having set up a tsconfig.json, we can process all specified files with the given options simply with tsc.

As well as the files option, there is also the similar include option. This allows you to specify all files in a given directory, or all files in all directory. For example:

will include all .ts files in the src directory, or:

will include main.ts and all .ts files in all sub-directories (**).

Full documentation on tsconfig.json can be found here.

Optional parameters

We can specify a parameter as optional through the use of ?. For example:

Note how the parameter greeting is marked with ?, which indicates it is optional. The logic of the greet() function displays the greeting if it exists, or "Hello" if it isn't provided, together with the value of the name parameter.

Optional properties in interfaces

We can also specify that certain properties of an interface are optional with ?. For example:

This means that variables conforming to the Animal interface must include a species, age and weight, but can optionally include a name. For example, both lion and binnie can both be initialised as objects conforming to the Animal interface, so can be explicitly declared as type Animal:

Exercise 3

Imagine we wanted to print out the name of the lion, e.g.:

  • What would happen if we attempted to compile and run the code?

Answer to exercise 3

This would not be a compilation/type-checking error, as Animal allows the optional property name. However, as the name property was not initialised for lion, it will have the type undefined. Therefore, undefined will be printed.

Arrays in TypeScript

If you have done COM431 (Data Structures, Algorithms and Mathematics) you will have been introduced to arrays. If not, you will have been introduced to Python lists in COM411 and Kotlin lists in COM534. As you should know, an array, or a list, is a variable which can hold multiple items of data.

In TypeScript, and indeed in JavaScript, arrays are extensible - they do not have fixed size. Thus, they are very similar to Python lists.

The data type of an array is specified using the notation []. So for example, the type number[] represents an array which can hold only numbers. string[] represents an array which can hold only strings.

For example:

Exercise 4

What type might you declare for an array which could store a mixture of strings and numbers?
  • Which would be the correct type?

Answer to exercise 4

(string | number)[] would do it, in other words an array which can contain either strings or numbers. Note the parentheses round string | number which evaluates the expression first.

string | number[] would be wrong, as that would declare the variable to be either a single string, or an array of numbers.

Likewise, string[] | number[] would also be wrong as it declares the variable as being either an array of strings only, or an array of numbers only. It does not cover the case where we want the array to hold a mixture of strings and numbers.

Generics with arrays

As an alternative syntax, which can be cleaner if arrays will hold multiple data types, we can use generics with an Array object. You have met generics already in Kotlin. If you remember, generics allow you to create generic data structures (hence the name) which can hold a user-defined type - in our example, generic arrays. Here is an example of using an array with generics.

We could then create a mixed string / number array with:

An introduction to Modules

We will revisit this topic later, but for now here is a quick introduction to TypeScript (and JavaScript) modules.

Often we might want to create some code and reuse it in different projects. Two examples that we have met today include TypeScript interfaces and also classes.

We can write these in separate files and export the contents, which can then be imported into other TypeScript/JavaScript files. These files are known as modules.

To do this, we need to add the keyword export to our interfaces and classes. For example, we could write our Animal interface in a file called animal.ts and export it:

We can then import the type Animal into another TypeScript file as:

We can do the same with classes, e.g. our Employee class that we looked at earlier:

and again import it:

Coding Exercises

You will be using the Node.js command prompt for these exercises.

Coding Exercise 1: Simple Use of TypeScript

For all of these questions, run tsc with --strict and --noEmit options to compile the appropriate file. If there are no errors, then run the file with tsx.

  1. Install TypeScript and tsx globally:
  1. Create a Hello World app in TypeScript, as shown in the notes above. Create a string variable (use let, so the value can be changed) with the contents "Hello World" and print it.

    Having type-checked with tsc and run with tsx, try to reset the variable to the number 123. Use tsc again. Does it work?

  2. Create a new TypeScript application containing a function to calculate a grade from a percentage mark according to the scheme below. The function should take a percentage (a number) as a parameter and the grade (a string) should be returned. Ensure that the function parameter and return value types are declared. Test it out by calling the function with a few different percentages, and printing the result.

  • 70-100 : A
  • 60-69 : B
  • 50-59 : C
  • 40-49 : D
  • 0-39 : F
  1. Adjust the previous question so that if the percentage is less than 0, or greater than 100, null is returned. What other changes to your code do you need to make to accommodate this?

Coding Exercise 2: A larger project

We will now develop a slightly larger project which simulates a university and its students - something we also looked at in OODD. This university has two types of students: regular Undergraduates, and EveningStudents who are members of the general public who come in once a week to study an evening class.

  1. Create a new folder to store this larger project which will work with music data. Inside this folder, create a tsconfig.json which sets both strict and noEmit to true and specifies to compile main.ts. Also in the tsconfig.json set allowImportingTsExtensions to true; this will allow you to use import with .ts files.

  2. Create a module types.ts containing an interface called Student which specifies id, name, course and email properties. The id can be either string or number. The name and course are both strings. Finally, email is a string, but is optional (students do not have to give their email address). Export the interface.

  3. Create a file main.ts and import the Student type into it. Create a function called printStudent() which takes a Student as a parameter and displays its id, name, course and email properties (or "no email provided" if there is no email). Also write code to create two Students (one with an email, and one without) and print them using printStudent().

  4. Create a file undergraduate.ts containing a class Undergraduate which implements the Student interface and also contains, as an attribute, a modules array (an array of the modules they are studying - each module is a string) and a method addModule() which adds a new module to the list of modules. Export the class.

    In the constructor, pass in the ID, name, course and modules array as parameters.

    It should also have a toString() method which returns a string containing the name, course, list of modules and email if it exists (or "no email provided" if it does not). To obtain the list of modules as a string, use this.modules.join(','): this joins each module in the array into a comma-separated list.

  5. Also create a file eveningstudent.ts containing a class EveningStudent which implements the Student interface and contains a toString() method which returns a string containing the ID, name, course and email if it exists (or "no email provided" if it does not). Again export the class, and again pass in the student's data in the constructor.

  1. Create a University class in a new file university.ts. The University class should have an attribute students which should be an array of objects implementing the Student interface.

    In the constructor, set this array to an empty array.

    Add an enrolStudent() method which adds a new student to the array of students.

    Add a findStudentById() method which takes in a student ID (number or string) as a parameter, loops through the students, and returns the student with that ID if it exists, or null if it does not. Ensure you specify the return type of the method.

    Export the class once again.

  2. In the main file main.ts import the three classes from their own files. Create a University object and two or three students, at least one of which is an Undergraduate and at least one of which is an EveningStudent. Enrol the students in the university and then try to find them using findStudentById(), and print them out with console.log().