An introduction to TypeScript and ES Modules

An introduction to TypeScript and ES Modules

09/17/2020 17:20:00

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.

But we should translate this into simpler English - a Type let's 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:

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.

// 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 - 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

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

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

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

    Hello world.


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

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

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

and replace our index.js with

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:

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?

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:

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:

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).

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.

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:

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:

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:

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

combineCoordinates(c1, c2);

But this

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:

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

function printCompassDirection(direction) {


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.

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

let variable = "blah";

and later overwrite that value with a number

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.