TypeScript For the COMP 301 Java Developer

Written by Ajay Gandecha for COMP 426: Modern Web Programming, with contributions from Kris Jordan, adapted from COMP 423: Foundations of Software Engineering.

Introduction

In the previous reading, you learned that we can break every website into three fundamental parts:

  1. Content: All webpages have content - this includes text, images, links, etc. that you see on the website.
  2. Style: The style of a website is how the content is presented to the user. This includes the colors, fonts, spacing, and layout of the content. Multiple webpages may share the exact same content, but can be styled differently.
  3. Behavior: The behavior of a website is how the website responds to user interactions. This includes animations, forms, and other complex logic such as logging in, storing / manipulating data, and more.

The different files that the browser receives from the server provide these three fundamental parts of the website - content, style, and behavior. The content of a website is defined using HTML (.html) files. The style of a website is defined using CSS (.css) files. The behavior of a website is defined using JavaScript (.js) files. In the previous two lectures, you learned about HTML and CSS.

JavaScript is the standard programming language used to power websites and web applications. Browsers have built-in JavaScript engines that can interpret and execute JavaScript code. According to the 2023 StackOverflow Developer Survey, developers ranked JavaScript as their most commonly-used programming language - an eleven-year long streak and counting.

In COMP 426, we will use TypeScript as our programming language to build web applications. TypeScript is a superset of JavaScript - it adds static typing with optional type annotations to JavaScript. Static typing allows us to catch type errors in our code before it runs, which helps to solve common, pesky runtime errors that developers can encounter in JavaScript. We will talk more about this property of TypeScript in-depth in lecture.

This document is designed to help you become familiar with the syntax and features of TypeScript from the context of the Java experience you all have had in COMP 301. It compares the syntax between Java and TypeScript in various situations and should serve as a good guide as you start using TypeScript in this class!

Syntax

The syntax for TypeScript is pretty succint and less verbose than Java. In this section, you will learn the syntax of TypeScript code with the context of the Java syntax you have worked in throughout COMP 210 and COMP 301.

Typing

The first major distinction between Java and TypeScript exists with its typing system. Recall the following:

  • Primitive types are a set of basic data types in a programming language. All other data types and classes can be constructed from these primitive types. Using standard programming conventions, primitive types are often denoted with an all-lowercase name and usually do not need to be imported.
  • Reference types, on the other hand, are all of the other types in a language. Reference types are defined as structures that contain or build upon the basic primitive types. Reference types are often defined by interfaces, classes, and enumerations. Reference types, like the name of all clases, often start with a capital letter (For example, Dog or Cat).

In Java, we have the following primitive types:

  • int: Represents a number with no decimal places.
  • double: Represents a number that can store fractions (decimal places).
  • boolean: Represents a state that can either be true or false.

In TypeScript, on the otherhand, we have different primitive types. TypeScript defines the following:

  • number: Represents a number that can store fractions (decimal places).
  • boolean: Represents a state that can either be true or false.

Notice there is not a type distinction between integers and floats / doubles. We use number in TypeScript for both. This is helpful because it effectively allows us to work with double-floating point, 64-bit, values for all numerical computations.

Second, notice that string is not capitalized in TypeScript. Technically, the string values we use wind up being references and realizations of the immutable String class in TypeScript, with its expected methods, but its type is specified with lowercase letters as a built-in language feature.

Variable and Constant Declarations

Now that you know a bit about the basic data types in TypeScript, let's take a look at how to define variables.

Let's compare a number declaration in Java and TypeScript, then compare more generally.

Java
// Declaring a Number
int myNumber = 88;

// General Formula
type name = value;
TypeScript
// Declaring a Number
let myNumber: number = 88;

// General Formula
let name: type = value;

As you can see, there are a few differences. First, in Java, we specify the data type first. In TypeScript, we provide a type annotation after the name of the variable. We also provide the let keyword before variable name.

You can also notice the difference in types. In Java, we use the int primitive type. In TypeScript, we use number instead. Lastly, note that both Java and TypeScript use semicolons at the end of their lines.

What if we wanted to make these values constants instead of variables (so that we cannot change their value later)?

Java
// Declaring a Constant
final int myNumber = 88;

// General Formula
final type name = value;
TypeScript
// Declaring a Constant
const myNumber: number = 88;

// General Formula
const name: type = value;

As you can see, in Java, we use the final keyword to turn a variable into a constant. The keyword is appended to the front. In TypeScript however, we just use the const keyword instead of the let keyword to define a constant.

Arrays

The way that arrays work in Java and TypeScript are a bit different, and so is the syntax to create them. In Java, you probably remember that the length of an array cannot be changed once it is set - and that using ArrayList<> or any other subtype of List (imported from java.utils.*) provides this functionality.

TypeScript arrays are more similar to the Java List than to the Java array. Creating arrays in TypeScript is also very similar to creating lists in Python. To declare an array in TypeScript, we can simply add [] to the end of a variable's type annotation and use brackets to add initial values. Compare the following:

Java
// Initialize
List<String> dogs = new ArrayList<>();
dogs.add("Corgi");
dogs.add("Lab");
// Add values
dogs.add("Husky");
// Replace a value
dogs.set("Samoyed", 2);
// Remove a value
// Removes by value
dogs.remove("Samoyed");
// Removes by index
dogs.remove(1);
// Access a value
String corgi = dogs.get(0);
TypeScript
// Initialize
let dogs: string[] = ["Corgi", "Lab"];
// Add values
dogs.push("Husky");
// Replace a value
dogs[2] = "Samoyed";
// Remove a value
// Removes by value
dogs.splice(
  dogs.indexOf("Samoyed"), 1
);
// Removes by index
dogs.splice(1, 1);
// Access a value 
let corgi: string = dogs[0];

Just like in Python lists and traditional Java arrays (but unlike Java's List), we can index values of TypeScript arrays using the subscription [] syntax.

As shown in the code above, TypeScript does not have a built-in delete method - but, it does have .splice(i, n), which removes n number of elements starting at index i. So, we can combine this with .indexOf() to delete our value.

TypeScript's arrays also have a .pop() method that removes the last item of an array.

To access the length of a TypeScript array, you can use the array's length field. For example, given an array a, the length of this array would be accessed using a.length.

Conditionals

Java and TypeScript have similar syntax for creating conditional statements and if-statements.

TypeScript uses the same boolean operators that Java does. This means that && represents AND, || represents OR, and ! represents NOT. TypeScript and Java both use the lowercased true and false for boolean values.

Additionally, like in Java, the && and || operators are short-circuiting. If the left-hand expression of an && operator is false, the right-hand expression will not be evaluated. Conversely, if the left-hand expression of an || operator is true, then the right-hand expression will not be evaluated. This matters when the right-hand expression contains a function or method call that mutates state.

If-statements have the same syntax and usage as they do in Java. Both Java and TypeScript require the use of parenthesis ( ) around the conditional statements in if-statements.

Java
if (conditionA || conditionB) {
  // Some code here!
}
else {
  // Some code here.
}
TypeScript
if (conditionA || conditionB) {
  // Some code here!
}
else {
  // Some code here.
}

While Loops

Just like with if-statements, both Java and TypeScript use the same syntax for while loops. We use parenthesis around the conditional in both languages.

Java
while (conditionA) {
  // Some code here!
}
TypeScript
while (conditionA) {
  // Some code here!
}

For Loops

In both Java and TypeScript, there are two types of loops that both serve distinct purposes.

The first type of loop contains a counter variable that is modified each time the the loop iterates - and, iteration stops when some provided condition evaluates to false. This type of loop exists in both Java and TypeScript. The code is nearly identical, but notice that in the TypeScript version, we need to use our new method of creating variables. We do not say int i = 0;, instead we say let i = 0;. We can see this here:

Java
for(int i = 0; i < 10; i++) {
  // Loop body here...
}
TypeScript
for(let i = 0; i < 10; i++) {
  // Loop body here...
}
Java
for(String name : names) {
  // Loop body here...
}
TypeScript
for(let name of names) {
  // Loop body here...
}

As you can see, like in previous examples, TypeScript uses the let keyword. In addition, Java uses :, while TypeScript uses of.

Defining Functions

Functions are the most fundamental abstraction technique we use in software engineering. It is important to note that in Java, we create methods, which are functions that are members of a class. In TypeScript, we also mainly work in the context of classes, but we are not necessarily required to. So, if you are hearing the term "functions" and "methods" passed around, it is useful to remember this distinction: methods are called on an object (e.g. object.method()) whereas functions are generally called standalone function(). This distinction has some nuance in more advanced uses of TypeScript, but is generally how you should approach it.

There are many fundamental differences in the syntax for creating functions in Java and TypeScript. Let's take a look at an example of a function that takes in a user's name and returns a string that greets the user.

Java
String greet(String name) {
  return "Welcome, " + name + "!";
}
TypeScript
function greet(name: string): string {
  return "Welcome, " + name + "!";
}

There are a few noticeable differences. First, TypeScript uses the function keyword at the front rather than specifying a return type first. Also, the type annotation is at the end of the function header (and before the body). The placement of type annotations for the function parameters also changes here.

In the case that a function returns nothing, note that in Java, we specify the return type to be void. We can do this in TypeScript too, however it is optional. Both including : void or not is valid. For example:

Java
void doSomething() {
  // Implementation Not Shown
}
TypeScript
function doSomething() {
  // Implementation Not Shown
}

// OR

function doSomething(): void {
  // Implementation Not Shown
}

Arrow Functions

TypeScript also has a tremendously useful feature called arrow functions. Arrow functions are a more compact and concise method of defining traditional functions. Let's take a look at a function from above as a traditional function and one as an arrow function.

Traditional Function
function greet(name: string): string {
  return "Welcome, " + name + "!";
}
Arrow Function
let greet = (name: string): string => {
  return "Welcome, " + name + "!";
}

There are a few things to unpack here. First, it looks like we are ultimately assigning "something" to a variable. We use the let keyword and we provide a variable name! On the right, we have a weird structure that would go in the value spot of our variable formula.

In fact, this is exactly what we are doing! We are saving a function to a variable and giving it a name that we can use to call it. In the ( ), we provide the parameters to the function. We provide a return type in the type annotation as well. Then, we use => to connect these parameters to a function body.

We can then call our function in the same way we would normally, like so:

Calling a Function
greet("Jade")

While this seems like just a syntactic change, the implications of this are massive and opens the door to an entire new world of programming called functional programming, as we can pass around functions as values. This is something that we will be covering extensively throughout this course, however it is super important to become familiar with the arrow function syntax now so it is less suprising later!

To conclude this section, provide two important caveats must be emphasized:

  • Arrow functions don't have their own this bindings and therefore should not be used when defining methods of a class.
  • Arrow functions cannot be used as constructors. Calling them with new throws a TypeError.

These caveats are important to note because traditional functions and arrow functions are not exactly the same, and there are some semantic differences.

Class and Interface Construction

Classes define data types and are the foundation of object-oriented programming. It will be critical for you to be comfortable working within TypeScript classes throughout your time in COMP 426! While there are many syntax differences between classes in Java and TypeScript, the core idea and motivation for using them remains the same. Below is an example of a full class in both Java and TypeScript. I recommend that you read this in its entirely and try to compare line by line! From there, we will go through each section.

Java
/** Represents a UNC Student. */
public class Student {

  /* Fields
  * NOTE: In COMP 301, you learned
  * about using the `private`
  * keyword on fields to control
  * access via getter and sette
  * methods. For this example,
  * I am making some fields
  * public and others private.
  */

  /** Name of the student */
  public String name;
  /**Year of the student*/
  public int year;
  /** Address of the student */
  private String address;

  /* Constructor */
  public Student(
      String name,
      int year,
      String adr
  ) {
      this.name = name;
      this.year = year;
      this.address = adr;

      this.welcome();
  }

  /* Methods */

  /** Prints a welcome message
  * to the console. */
  public void welcome() {
      System.out.println(
          "Hello, " + this.name + "!"
      );
  }

  /** Converts a year number
  * to a description. */
  public static String yearToString(
      int year
  ) {
      if(year == 1) {
          return "Freshman";
      }
      else if(year == 2) {
          return "Sophomore";
      }
      else if(year == 3) {
          return "Junior";
      }
      else if(year == 4) {
          return "Senior";
      }
      return "Oops...";
  }
}
TypeScript
/** Represents a UNC Student. */
public class Student {

  /* Fields
  * NOTE: In COMP 301, you learned
  * about using the `private`
  * keyword on fields to control
  * access via getter and sette
  * methods. For this example,
  * I am making some fields
  * public and others private.
  */

  /** Name of the student */
  public name: string;
  /** Year of the student*/
  public year: number;
  /** Address of the student */
  private address: string;

  /* Constructor */
  constructor(
      name: string,
      year: number,
      adr: string
  ) {
      this.name = name;
      this.year = year;
      this.address = adr;

      this.welcome();
  }

  /* Methods */

  /** Prints a welcome message
  * to the console. */
  public welcome() {
      console.log(
          "Hello, " + this.name + "!"
      );
  }

  /** Converts a year number
  * to a description. */
  public static yearToString(
      year: number
  ): string {
      if(year == 1) {
          return "Freshman";
      }
      else if(year == 2) {
          return "Sophomore";
      }
      else if(year == 3) {
          return "Junior";
      }
      else if(year == 4) {
          return "Senior";
      }
      return "Oops...";
  }
}

As you can see, there are a few similarities between classes in Java and TypeScript! First, we can look at access modifiers. The public, private, and protected keywords are the same in both Java and TypeScript.

Notice that the fields are the same conventions as well! Note however that the let keyword is not used when defining fields - it is only needed when defining regular variables. The constructor also differs a bit. In TypeScript, the constructor keyword replaces public ClassName from Java. The type annotations in the parameters also follow the normal conventions of TypeScript functions.

Lastly, like fields, methods in TypeScript also do not use their respective keyword (function) to be defined. Instead, we can just provide an access modifier.

We use the static keyword to denote class methods the same way in both Java and TypeScript. Learn more about class fields and methods here.

Within a function, we also have access to the this keyword that references the current object.

Now, how do we instantiate objects?

We actually use the same syntax as in Java:

Java
Student noah = new Student(
  "Noah", 3, "Columbia St"
);
TypeScript
noah: Student = new Student(
  "Noah", 3, "Columbia St"
);

We can also define interfaces in TypeScript like we do in Java. Take a look at the following:

Java
interface Person {
  String name;
}

class Student implements Person { }
TypeScript
interface Person {
  name: string;
}

class Student implements Person { }

As you can see, the keywords remain the same between both languages with interface and implements! The only difference between the two languages are with variable creation and type annotation conventions.

There is also another interesting feature of TypeScript worth mentioning here.

TypeScript employs structural type checking. This means that TypeScript views objects as equivalent types if they share the same structure, NOT just the same name! On the otherhand, Java is a nominally type language, which means it views objects as equivalent types if they share the same name ONLY (or if there is an inheritence relationship).

So, we can technically directly create a value of type Person in TypeScript! This is not something you can directly do in Java without creating a subclass. The syntax would look like so:

TypeScript
let person: Person = {
  name: "Charles"
};

In this example, we use JSON (JavaScript object notation) to create an object of data that contains the same properties that a Person objct should have. Surprisingly enough, due to TypeScript being a structural language, this is a valid way to instantiate an object, technically of type object, that can used anywhere an object of type Person is expected without explicitly implementing the Person interface!

This feature is often called "duck typing", thanks to the addage "if it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck." TypeScript and structural typing take it further: "if it's a goose that looks like a duck, then it's a duck." In programming, structural type checking like TypeScript's, relaxes the strictness of nominal typing like Java's, by embracing the idea that if an object has all the same fields and methods needed as some other type, then it's probably fine to treat it as that other type.

Extra TypeScript Features

Comments

The examples throughout this document have already used many comments, however we create comments in Java and TypeScript in the exact same way! This is shown below:

Java
// This is a single-line comment.

/*
This is an example of a
multi-line comment!
*/
TypeScript
// This is a single-line comment.

/*
This is an example of a
multi-line comment!
*/

Printing Values

In Java and TypeScript, we have statements to print out values! In TypeScript, values are printed to the console. We use the following convention to print values:

Java
String dog = "Corgi";
System.out.println(dog);
// Output:
// >> Corgi
TypeScript
let dog: string = "Corgi";
console.log(dog);
// Console:
// >> Corgi

As you can see, we use console.log() to print out values to the console in TypeScript. To see values printed to console.log() in a browser, you will need to open your browser's developer tools and view its console tab.

Enums

Enums (enumerators) are an extremely useful language feature in many programming languages! Enums allow you to define custom, related values or states. Think of enums as implementing a multiple-choice question, where there are many options! Let's look at an example:

Java
enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT,
}
TypeScript
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

As you might notice, creating enums in TypeScript is nearly equivalent to its Java counterpart that you saw in COMP 301! The main difference is that it is convention for Java enum options to be entirely capitalized, while TypeScript enum options only have their first letter capitalized.

Let's look at the Direction enum applied in a TypeScript function:

TypeScript
const directionToText = (direction: Direction): string => {
  if(direction == Direction.Up || direction == Direction.Down) {
      return "Let's go vertically!"
  }
  else if (direction == Direction.Left || direction == Direction.Right) {
      return "Let's go horizontally!"
  }
}

Enumerations will be extremely useful in your final projects to model data.

Type Aliases

There is a nifty feature in TypeScript called the type alias, which essentially allows you to create another label by which you can refer to a type. This can be useful to make types more concise, or to make it more readable for your feature. Look at the following example:

TypeScript
type Rating = number;

let shopRating: Rating = 10;

Using the type keyword, we give number an alias as Rating. Now, we can use number and Rating interchangeably. Next, we create a variable called shopRating of type Rating (which is really just type number), and then assign a number to it.

Ternary Operator

The last super useful feature of TypeScript to feature in this document is the ternary operator. The ternary operator allows you to write a conditional expression. Unlike the if/else syntax in TypeScript and Java, which are statements, the ternary operator results in an expression. This means that if a condition is true, the expression can evaluate to one value and if it's false, another.

The ternary operator uses the following syntax: condition ? expr_if_true : expr_if_false

TypeScript
// Stores the hour which the boba shop opens.
// For sake of example, say the boba shop opens
// at 10am on weekdays and 12pm on weekends:
let shopOpeningHour: number = isWeekday ? 10 : 12;

console.log(shopOpeningHour);

// Output IF isWeekday = true:
// >> 10
// Output IF isWeekday = false:
// >> 12

// Since the ternary operator produces an expression, it can
// also be used like:
console.log(isWeekday ? "Weekday" : "Weekend")

This is the same syntax that is used in Java.

Generic Types

Generic types are a powerful convention in Java that allows you to pass types as a parameter into objects. This makes objects support multiple data types.

For example, take a LinkedList implementation in Java. Linked lists are data structures that can store many different types of values. For example, look at the following in Java:

Java
// Create a linked list that stores strings.
LinkedList<String> myStringList = new LinkedList<>();
// Create a linked list that stores students.
LinkedList<Student> myRoster = new LinkedList<>(); 
console.log(isWeekday ? "Weekday" : "Weekend")

The above Java syntax likely looks vaguely familiar. Here, we are creating two linked lists - one of String objects and the other of Student objects. We pass the data type of the object we want to store into the < > part of the type annotation.

TypeScript also supports generic types! This is a feature that will be used a lot throughout this course. First, let's compare the syntax for creating the hypothetical lists shown above:

Java
// Create a list that stores strings.
LinkedList<String> myStringList =
  new LinkedList<>();
// Create a list that stores students.
LinkedList<Student> myRoster =
  new LinkedList<>(); 
TypeScript
// Create a list that stores strings.
let myStringList: LinkedList<string> =
  new LinkedList<>();
// Create a list that stores students.
let myRoster: LinkedList<Student> =
  new LinkedList<>(); 

As you can see, the only thing different between both code snippets are how we declare the variable. The usage of < > remains the same.

Now, how would we actually implement the LinkedList<T> class? Let's compare two rudimentary implmentations in both Java and TypeScript:

Java
/** Represents a linked list node. */
public class LinkedList<T> {

  /** Value for the node. */
  private T value;
  /** Next node, if it exists. */
  private LinkedList<T> next;

  /** Constructor */
  public LinkedList(T value) {
      this.value = value;
  }

  /** Returns the node's value. */
  public T getValue() {
      return this.value;
  }

  /* Modifies the node's value. */
  public void setValue(T value) {
      this.value = value;
  }

  /* Other methods not shown */
}
TypeScript
/** Represents a linked list node. */
public class LinkedList<T> {

  /** Value for the node. */
  private value: T;
  /** Next node, if it exists. */
  private next: LinkedList<T>;

  /** Constructor */
  constructor(value: T) {
      this.value = value;
  }

  /** Returns the node's value. */
  public getValue(): T {
      return this.value;
  }

  /* Modifies the node's value. */
  public setValue(value: T) {
      this.value = value;
  }

  /* Other methods not shown */
  }

In the header of the class, we put <T>, which specifies that we are adding a type parameter! Whenever this is then provided, like in LinkedList<string> or LinkedList<Student>, the T used throughout the class is then replaced by the type that is provided! So in the LinkedList<string> example, the field value becomes of type string. In the LinkedList<Student> example, the field value becomes of type Student.

If this concept is unfamiliar, we highly recommend to practice using generic types in classes, as well as experimenting on your own! Generic types are an invaluable tool to make code extendable to multiple use-cases, and is used in many of the packages we are going to use throughout the course.

Wrapping Up

TypeScript is an extremely powerful and useful language, and we you will have the chance to work with TypeScript code this entire semester. If you are having trouble remembering TypeScript syntax, feel free to return to this document at any time. In addition, for practice, we highly recommend you go to the official TypeScript playground or open a new Repl.it! The TypeScript playground, as well as opening a .ts TypeScript file in Visual Studio Code and playing around with it, are some of the best ways to get more familiar and accustomed to using the language. As you have seen throughout your computer science careers so far, sometimes the best way to learn is to dive right in!

In addition, below are some additional resources that you may find useful as your work through learning TypeScript.

Extra Resources