An introduction to TypeScript and ES Modules
David Whitney
Posted on September 17, 2020
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
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
:
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">
<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
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) {
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.
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.
Posted on September 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.