ICSSC LogoICS Student Council
Getting Started

JavaScript and TypeScript

JavaScript is a high-level programming language used in many places, but particularly in web development as "the language of the Web." TypeScript builds on top of JavaScript by providing a type system, ensuring that certain kinds of errors never make it to runtime.

The overwhelming majority of projects developed and maintained by ICSSC Projects use TypeScript as their primary language. This is to ensure that anyone who is interested in contributing to one project may, with minimal effort, also contribute to another project.

Below, we'll provide a quick overview of the basics of JavaScript, including some patterns that we use frequently in our codebases. We'll then introduce TypeScript's type system and how it's enforced.

In this and future parts of the Getting Started guide, we will introduce new terms in italics.

Any points that are particularly important, which we really want you to take away from this guide, will be in bold.

Supplementary notes, like this one, will be enclosed in a box like this called a Callout.

Introduction to JavaScript

Printing

As a product of the Web, the primary way that JavaScript interacts with standard input and output is through an object called console, which as its name suggests is an interface to your browser's developer console. console is used even in server-side code, except that it writes to the terminal instead.

console.log("Hello, world!");

Variables

Unlike some other languages, you can declare constants in JavaScript using the keyword const, and JavaScript will make sure that it cannot be modified. It's recommended that you use const as much as possible, and only use the keyword let for things that absolutely must change at some point (for instance, loop counters).

In some older code you may also see the keyword var. The use of var is not considered best practice anymore, so you should avoid using it. let does the same thing in 99% of cases.

let s = "foo";
s = "bar"; // this is OK
 
const pi = 3.14;
pi = 6.28; // this causes an error

Strings and string interpolation

Strings function much the same in JavaScript as in any other programming language, but one feature that JavaScript has that some others may not is string interpolation. This allows you to put any expression or value into a string, while enhancing readability because the value or expression is located where it will appear in the string, rather than after.

const likes = 5;
console.log(`This post has ${likes} likes.`); // This post has 5 likes.
console.log(`I wish I had ${likes * 10} likes!`); // I wish I had 50 likes!

Equality

JavaScript has two types of equality operators, == and === (and their negations, != and !==). Due to historical reasons, == and != exhibit distinctively non-intuitive behavior. For example:

console.log("" == false) // true - but why?????
console.log("" === false) // false - obviously

For this reason, you should almost always use === and !==.

Functions: arrows, anonymity, oh my!

While we're on the subject of JavaScript having many ways to do almost (but not quite) the same thing, there are many ways to declare a function, each with their own quirks. We won't be going into too much detail here, and will only make our recommendations on which ones to use (and which are commonly used in our projects' codebases).

You can declare a function the classic way, using the function keyword:

function sayHello() {
    console.log("Hello, world!");
}

But you can also declare an anonymous function and assign it to a variable:

const sayHello = function () {
    console.log("Hello, world!");
}

Needless to say, this pattern is verbose for no real good reason, and should almost never be used.

A pattern that looks like this, but is more concise and thus very commonly used, is the arrow function:

const sayHello = () => {
    console.log("Hello, world!")
}

If your arrow function returns the value of a single expression, it can be made even more concise by eliminating the curly braces, like so:

const cylinderVolume = (r, h) => Math.PI * (r ** 2) * h;

Functions are an extremely powerful part of JavaScript: they can be passed as arguments to other functions, which can enable you to write extremely clean and easily maintainable code. You'll see more examples of code written in this fashion, known as functional programming, below.

Arrays and comparators

One major quirk (and one that causes beginners hours of pain) of JavaScript arrays is that by default, its .sort() method sorts the array in alphabetical order as if the elements were strings, even if they are not. This quirk is easily illustrated like so:

const numbers = [3, 2, 11, 1, 4];
// you might expect 11 to be last...
numbers.sort()
// ...but it's not:
console.log(numbers) // [1, 11, 2, 3, 4] - boo

We can override this default (and frankly nonsensical) behavior by providing our own comparator function. This function just takes in two elements, and depending on their values in relation to each other, returns another value.

compareFunction(a, b) return valueResulting sort order
a positive numbersort b before a
a negative numbersort a before b
zeroif a was first, keep it that way

In our instance, we can have our array be sorted properly by using a simple arrow function that just subtracts b from a, so the ordering is correct:

const numbers = [3, 2, 11, 1, 4];
numbers.sort((a, b) => a - b);
console.log(numbers) // [1, 2, 3, 4, 11] - yay!

Array methods

JavaScript arrays also come with a multitude of methods that can be used to do things with the array's contents. The most commonly used method is .map():

const numbers = [1, 2, 3, 4];
const double = (x) => x * 2;
const doubled = numbers.map(double);
console.log(doubled) // [2, 4, 6, 8]

That's cool and all, but let's look at a problem that's a bit more complicated. Suppose you have an array of numbers, and you want to write a function that returns the sum of all the even numbers in the array. You could probably accomplish this task with loops, like so:

const numbers = [1, 2, 3, 4];
function sumOfEvens(nums) {
    let sum = 0;
    for (let i = 0; i < nums.length; ++i) {
        if (nums[i] % 2 === 0) {
            sum += nums[i];
        }
    }
    return sum;
}
console.log(sumOfEvens(numbers)); // 6

But what if I told you this could be a one-line arrow function?

const numbers = [1, 2, 3, 4];
const sumOfEvens = (nums) => nums.filter((num) => num % 2 === 0).reduce((acc, i) => acc + i, 0);
console.log(sumOfEvens(numbers)); // 6

Don't just take my word for it - paste this into your browser console or Node.js window, and see if it's true. This is the power of JavaScript's array methods, and more broadly, functional programming. For this reason, array method chaining is used quite a bit throughout our codebases.

However, it's important to recall that functional programming is not a cure-all: there are some places where loops may be more readable and make more sense than chaining array methods, and that's fine!

Objects

Objects in JavaScript are commonly used in a similar way to dictionaries or hash maps in other languages. Unlike some other languages, however, you don't need quotes around the keys. Also, you can access the values using either bracket or dot notation. This makes objects in JavaScript function similar to data classes or data transfer objects in other languages.

const ages = { alice: 40, bob: 35, carol: 13 };
console.log(ages["alice"]); // 40
console.log(ages.bob); // 35

If you have a constant you'd like to turn into a key of an object, you can do that too by surrounding it with square brackets, like so:

const name = "dan";
const ages = { [name]: 55 };
console.log(ages.dan); // 55

Introduction to TypeScript

Now that you're more familiar with JavaScript, let's bring on some additional complexity with TypeScript.

Built-in types

TypeScript understands that JavaScript has a few built-in types, even if it doesn't have type checking. Its built-in type system is thus built upon these types:

Let's look at a few examples:

  • boolean and string are as you would expect in any other language.
  • number represents any number, integer or floating-point - JavaScript doesn't make a distinction, so neither does TypeScript.
  • undefined and null are special types that represent a value which doesn't exist. undefined is the default value of any variable that hasn't been initialized while null must be assigned deliberately. However, a variable of type undefined cannot have the value null, and vice versa.
  • void is the return type of a function that doesn't return anything.
  • never is the return type of a function that never returns, i.e. always throws an error or terminates the program.
  • unknown is used to denote data whose type and structure are not known.
  • any is similar to unknown, but overrides type checking entirely for that object. As it defeats the purpose of TypeScript, it should be used sparingly.

Assigning types

Generally, TypeScript is smart enough to figure out the types of new variables based on what value they have. Therefore, unless the inferred type is wrong, you don't really need to declare the type explicitly.

However, if you use a let declared variable with no initial value, TypeScript will likely require you to explicitly declare the type.

Function parameters should almost always have types, since that's how you can provide information to whoever is using your function on what types the arguments of your function takes.

const aNumber: number = 42;
const anotherNumber = 100; // the type of `anotherNumber` is automatically inferred as `number`
 
let x; // no type inference going on here
x = 2;
 
function addNumbers(
  a: number,
  b: number // parameters should have types...
) { // but return types don't necessarily need to be explicitly declared
  return a + b;
}

Object types

Aside from built-in types, TypeScript also supports typing the values of objects. This is immensely powerful; with it, you can model data in a type-safe manner, ensuring that you don't accidentally access a property that doesn't exist, or write data of an incompatible type to a field.

There are two main ways to declare object types, type aliases and interfaces:

type User = {
  name: string;
  id: number;
}
interface User {
  name: string;
  id: number;
}

While they look similar, type aliases and interfaces have some subtle syntactic differences. Each ICSSC project may have its own convention on when to use type aliases or interfaces (and when not to), so it's important to follow the project's conventions.

Unions

Unions allow us to create a type that can be any one of a list of types which we provide. The most straightforward application of this is in creating something akin to enums in other languages:

type PrimaryColors = "Red" | "Yellow" | "Blue";

You may or may not have noticed that enums do exist in TypeScript! However, like with var, their use is not recommended.

Another pattern that is commonly seen in some of our projects is the tagged union. The tagged union is commonly implemented as a union of several objects that are otherwise different, but which share one property. It is extremely powerful because it's extremely easy to discern which object you're dealing with just by checking the shared property.

type SuccessResponse = { success: true, message: string };
type FailureResponse = { success: false, error: string };
type ResponseObject = SuccessResponse | FailureResponse;
 
function processResponse(res: ResponseObject) {
  if (res.success) {
    console.log(`Success! Message was: ${res.message}`);
  } else {
    console.log(`Failure! Error was: ${res.error}`);
  }
}

On this page