An introduction to TypeScript and ES Modules
Tags: javascript, typescript, webdev, node
Published: 2020-09-17 16:26:32

JavaScript is everywhere, and TypeScript is JavaScript with some cool extra features.

You've probably heard of it, it's exceptionally popular, with lots of really mainstream JavaScript libraries and frameworks being built in TypeScript.

We're going to go through what a `type` is, why they're useful, and how you can use them without getting lost in configuration and tools.

First, let's understand what TypeScript is -

> TypeScript extends JavaScript by adding types.
>
> By understanding JavaScript, TypeScript saves you time catching errors and providing fixes before you run code.
>
> Any browser, any OS, anywhere JavaScript runs. Entirely Open Source.

TypeScript is a programming language that is a superset of JavaScript - any valid JavaScript, is valid TypeScript - and it adds additional language features that get compiled down to vanilla JavaScript before it runs in your web browser. The most notable thing it adds to the language are `types`.


## What are types?

The TypeScript pitch is pretty simple - "JavaScript with Types, to help prevent you making mistakes in your code" - but when you start to google around what Types are, you end up with things like the [wikipedia page on computational type theory](https://en.wikipedia.org/wiki/Type_theory).

But we should translate this into simpler English - a `Type` lets you tell the computer that you expect data in a specific "shape", so that it can warn you if you try to use data that isn't in the correct format.

For example, this is an `interface`:

```ts
inteface Animal {
    numberOfLegs: number,
    numberOfEyes: number
}
```

This `interface` is a `Type definition` - that says:

- Animals have two properties.
- numberOfLegs, which is a number
- numberOfEyes, which is a number

In `TypeScript` you can just put an `interface` like that in your `.ts` files. 

A `.ts` file? Well *that* is identical to a regular `JavaScript` `.js` file - that also has `TypeScript` code in it.

When we create a `JavaScript object` that contains the properties or functions that we've declared in our `interface`, we can say that our `object implements that interface`. Sometimes you'll see people say the "object conforms to that interface". 

In practice, this means that if you create an object, for it to be an `Animal` and be used in your code in places that require an animal, it must **at least** have those two properties.

```js
// Just some object

const notAnAnimal = {
    blah: "not an animal"
};

// Cats are animals

const cat = {
    numberOfLegs: 4,
    numberOfEyes: 2
};

// You can even tell TypeScript that your variable
// is meant to be an animal with a Type Annotation.

const cat2: Animal = {
    numberOfLegs: 4,
    numberOfEyes: 2
};
```

We'll work some examples later on, but I'd rather look at what `TypeScript` can **do** for you.

Let's start by working out how we're going to **run our TypeScript code in our browser**.

# Running TypeScript in our browser with snowpack

Snowpack is a frontend development server - it does similar things to `CreateReactApp` if you're familiar with `React` development. It gives you a webserver that reloads when you change your files.

It's built to help you write your webapps using [ES Modules](https://flaviocopes.com/es-modules/) - that's where you can use `import` statements in your frontend code, and the browser does the work of loading JavaScript files from your server and making sure that requests don't get duplicated.

It also natively, and transparently supports `TypeScript` - this means you can add TypeScript files (with the extension .ts) and load them as if they're just plain old JavaScript. This means if you have all your code in a file called `index.ts`, you can reference it from a `HTML file` as `index.js` and it'll just work without you doing anything at all.


## Setting up snowpack

`snowpack` is available on NPM, so the quickest way we can create a project that uses `snowpack` is to `npm init` in a new directory.

First, open your terminal and type

```bash
npm init
```

Just hit enter a few times to create the default new node project. Once you have a `package.json`, we're going to install our dependencies

```
npm install snowpack typescript --save-dev
```

That's it!

Snowpack just works out of the current directory if you've not configured anything.

We can just go ahead and create HTML, JavaScript or TypeScript files in this directory and it'll "just work". You can run snowpack now by just typing

```
npx snowpack dev
```

# ES Modules, the simplest example

Let's take a look at the simplest possible example of a web app that uses `ES Modules`

If we were to have a file called `index.html` with the following contents

```html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Introduction to TypeScript</title>
    <script src="/index.js" type="module"></script>
</head>

<body>
    Hello world.
</body>

</html>
```
You'll notice that where we're importing our `script`, we're also using the attribute `type="module"` - telling our browser that this file contains an `ES Module`.

Then an `index.js` file that looks like this

```js
console.log("Oh hai! My JavaScript file has loaded in the browser!");
```

You would see the console output from the `index.js` file when the page loaded.

```
Oh hai! My JavaScript file has loaded in the browser!
```

You could build on this by adding another file `other.js`

```js
console.log("The other file!");
```

and replace our `index.js` with 

```js
import "./other";

console.log("Oh hai! My JavaScript file has loaded in the browser!");
```
Our output will now read:

```
The other file!
Oh hai! My JavaScript file has loaded in the browser!
```

This is because the `import` statement was interpreted by the browser, which went and downloaded `./other.js` and executed it before the code in `index.js`.

You can use `import` statements to import `named exports` from other files, or, like in this example, just entire other script files. Your browser makes sure to only download the `imports` once, even if you `import` the same thing in multiple places.

ES Modules are really simple, and perform a lot of the jobs that people were traditionally forced to use bundlers like `webpack` to achieve. They're deferred by default, and perform really well.

## Using TypeScript with snowpack

If you've used `TypeScript` before, you might have had to use the compiler `tsc` or `webpack` to compile and bundle your application.

You need to do this, because for your browser to run `TypeScript` code, it has to first be *compiled to JavaScript* - this means the `compiler`, which is called `tsc` will convert each of your `.ts` files into a `.js` file.

Snowpack takes care of this compilation for you, transparently. This means that if we rename our `index.js` file to `index.ts` (changing nothing in our HTML), everything still just works.

This is excellent, because we can now use TypeScript code in our webapp, without really having to think about any tedious setup instructions.

# What can TypeScript do for you right now?

TypeScript adds a lot of features to `JavaScript`, but let's take a look at a few of the things you'll probably end up using the most, and the soonest. The things that are immediately useful for **you** without having to learn all of the additions to the language.

TypeScript can:
- Stop you calling functions with the wrong variables
- Make sure the shape of JavaScript objects are correct
- Restrict what you can call a function with as an argument
- Tell you what types your functions returns to help you change your code more easily.

Let's go through some examples of each of those.

## Use Type Annotations to never call a function with the wrong variable again

Look at this addition function:

```js
function addTwoNumbers(one, two) {
    const result = one + two;
    console.log("Result is", result);
}

addTwoNumbers(1, 1);
```

If you put that code in your `index.ts` file, it'll print the number 2 into your console. 

We can give it the wrong type of data, and have some weird stuff happen - what happens if we pass a string and a number?

```js
addTwoNumbers("1", 1);
```

The output will now read `11` which isn't really what anyone was *trying* to do with this code.

Using `TypeScript Type Annotations` we can stop this from happening:

```ts
function addTwoNumbers(one: number, two: number) {
    const result = one + two;
    console.log("Result is", result);
}
```
If you pay close attention to the function parameters, we've added `: number` after each of our parameters. This tells TypeScript that this function is intended to only be called with `numbers`.

If you try and call the function with the wrong `Type` or paramter - a `string` rather than a number:

```js
addTwoNumbers("1", 1); // Editor will show an error here.
```

Your Visual Studio Code editor will underline the "1" argument, letting you know that you've called the function with the wrong `type` of value - you gave it a `string` not a `number`.

This is probably the first thing you'll be able to helpfully use in `TypeScript` that'll stop you making mistakes.

## Using Type Annotations with more complicated objects

We can use `Type annotations` with more complicated types too!

Take a look at this function that combines two `coordinates` (just an object with an `x` and a `y` property).


```js
function combineCoordinates(first, second) {
    return {
        x: first.x + second.x,
        y: first.y + second.y
    }
}

const c1 = { x: 1, y: 1 };
const c2 = { x: 1, y: 1 };

const result = combineCoordinates(c1, c2);
```

Simple enough - we're just adding the x and y properties of two objects together. Without `Type annotations` we could pass objects that are completely the wrong shape and crash our program.

```js
combineCoordinates("blah", "blah2"); // Would crash during execution
```

JavaScript is `weakly typed` (you can put any type of data into any variable), so would run this code just fine, until it crashes trying to access the properties `x` and `y` of our two strings.

We can fix this in `TypeScript` by using an `interface`. We can decalre an interface in our code like this:

```ts
interface Coordinate {
    x: number,
    y: number
}
```

We're just saying "anything that is a coordinate has an x, which is a number, and a y, which is also a number" with this `interface` definition. Interfaces can be described as `type definitions`, and `TypeScript` has a little bit of magic where it can infer if any object fits the shape of an `interface`.

This means that if we change our `combineCoordinates` function to add some `Type annotations` we can do this:

```ts
interface Coordinate {
    x: number,
    y: number
}

function combineCoordinates(first: Coordinate, second: Coordinate) {
    return {
        x: first.x + second.x,
        y: first.y + second.y
    }
}
```

And your editor and the TypeScript compiler will throw an error if we attempt to call that function with an object that doesn't fit the shape of the interface `Coordinate`.

The cool thing about this `type inference` is that you don't have to tell the compiler that your objects are the right shape, if they are, it'll just work it out. So this is perfectly valid:

```js
const c1 = { x: 1, y: 1 };
const c2 = { x: 1, y: 1 };

combineCoordinates(c1, c2);
```

But this

```js
const c1 = { x: 1, y: 1 };
const c2 = { x: 1, bar: 1 };

combineCoordinates(c1, c2); // Squiggly line under c2
```

Will get a squiggly underline in your editor because the property `y` is missing in our variable `c2`, and we replaced it with `bar`.

This is awesome, because it stops a huge number of mistakes while you're programming and makes sure that the right kind of objects get passed between your functions.

## Using Union Types to restrict what you can call a function with

Another of the really simple things you can do in `TypeScript` is define `union types` - this lets you say "I only want to be called with one of these things".

Take a look at this:

```ts
type CompassDirections = "NORTH" | "SOUTH" | "EAST" | "WEST";

function printCompassDirection(direction) {
    console.log(direction);
}

printCompassDirection("NORTH");
```

By defining a `union type` using the `type` keyword, we're saying that a `CompassDirection` can **only be** one of NORTH, SOUTH, EAST, WEST.  This means if you try call that function with any other string, it'll error in your editor and the compiler.

## Adding return types to your functions to help with autocomplete and intellisense

IntelliSense and Autocomplete are probably the best thing ever for programmer productivity - often replacing the need to go look at the docs. Both VSCode and WebStorm/IntelliJ will use the `type definitions` in your code to tell you what parameters you need to pass to things, right in your editor when you're typing.

You can help the editors out by making sure you add `return types` to your functions.

This is super easy - lets add one to our `combineCoordinates` function from earlier.

```ts
function combineCoordinates(first: Coordinate, second: Coordinate) : Coordinate {
    return {
        x: first.x + second.x,
        y: first.y + second.y
    }
}
```

Notice at the end of the function definition we've added `: Coordinate` - this tells your tooling that the function returns a `Coordinate`, so that if at some point in the future you're trying to assign the return value of this function to the wrong type, you'll get an error.

Your editors will use these type annotations to provide more accurate hints and refactoring support.

# Why would I do any of this? It seems like extra work?

It is extra work! That's the funny thing.

`TypeScript` is more verbose than `JavaScript` and you have to type extra code to add `Types` to your codebase. As your code grows past a couple of hundred lines though, you'll find that errors where you are *providing the wrong kind of data to your functions* or verifying that API calls return data that is in the right shape dramatically reduce.

Changing code becomes easier, as you don't need to remember every place you use a certain shape of object, your editor will do that work for you, and you'll find bugs sooner, again, with your editor telling you that you're using the wrong type of data before your application crashes in the browser.

# Why is everyone so excited about types?

People get so excited, and sometimes a little bit militant about types, because they're a great tool for removing entire categories of errors from your software. JavaScript has always had `types`, but it's a `weakly typed` language.

This means I can create a variable as a string

```js
let variable = "blah";
```
and later overwrite that value with a number
```js
variable = 123;
```
and it's a perfectly valid operation because the `types` are all evaluated while the program is running - so as long as the data in a variable is in the "correct shape" of the correct `type` - when your program comes to use it, then it's fine.

Sadly, this flexibility frequently causes errors, where mistakes are made during coding that become increasingly hard to debug as your software grows.

Adding additional type information to your programs reduces the likelihood of errors you don't understand cropping up at `runtime`, and the sooner you catch an error, the better.

# Just the beginning

This is just the tip of the iceberg, but hopefully a little less intimidating than trying to read all the docs if you've never used TypeScript before, without any scary setup or configuration.