As you probably know, TypeScript is a programming language that is a typed superset of JavaScript which compiles to plain JavaScript. Learning the language isn’t that difficult, but where do you start? In today’s world of front-end development, learning the language is only one part of the puzzle. In addition, you need to set up a system that somehow produces a production-ready deliverable from your source code. So how do you set this up? How do you go from plain JavaScript files (maybe in an existing code base) to a professional build output? This tutorial shows one way of doing this, using webpack.
This tutorial is a summary of a TypeScript workshop that I held at VX Company, together with Ferdi Meijer and Jeroen van den Belt. If you learn anything from this tutorial, the kudos are just as much for them as they are for me. If you find any problems, it’s all my fault :)
Table of contents
- Preparations
- Learning TypeScript
- Let’s add some structure
- Make it scalable
- Configure webpack
- Modules and imports
- Debugging TypeScript
- Using external libraries
- Conclusion
Preparations
You don’t need much to get started, but I assume you have the following installed:
- A relatively recent installation of Node.js.
- Your editor of choice (Visual Studio Code is my choice of the day, but any text editor will do).
Learning TypeScript
To start learning TypeScript, follow the excellent TypeScript in 5 minutes tutorial that can be found on the TypeScript website itself. This will cover the basics of the language, and set up a starting point for the remainder of this tutorial.
Let’s add some structure
Let’s continue where the TypeScript in 5 Minutes tutorial stopped. One of the first improvements you can make, is to make use of the Person interface in the Student class. Right now, there is no relation, but he Student class actually does implement the Person interface, so let’s make this explicit:
class Student implements Person
(If you’re new to TypeScript, it might not be immediately obvious that Student does implement the Person interface, but the cue is in the public
keywords in the constructor parameters. This is a shorthand for creating and initializing a property.)
Next, let’s add some structure to the project itself. Right now, all TypeScript source files are in one folder, and the compiled JavaScript output gets saved in this same folder. This will get quite messy when your project grows.
- Create a folder called
src
and move the TypeScript source files to this folder. - Create a folder called
dist
. This will be the output folder where the compiled JavaScript files will be written to. - Tell the TypeScript compiler that it needs to write the compiled files to this new
dist
folder. To do this, open thetsconfig.json
file and set the value of"outDir"
to"./dist"
.
If you compile your project now, you will see that TypeScript will find your source files in src
and write the compiled files to dist
.
To make this work in the browser, update your greeter.html
file such that it now points to the new location of the .js
files:
<script src="dist/greeter.js"></script>
Finally, it is good practice to have only one object per file, instead of having all the code in the one greeter.ts
source file. This is easily fixed: create two new files, one called student.ts
and one called person.ts
, and move the code for the Student class and Person interface to these respective files. Update the HTML file to include all the generated JavaScript files:
<script src="dist/person.js"></script>
<script src="dist/student.js"></script>
<script src="dist/greeter.js"></script>
Make it scalable
At this point, things are starting to look more and more Production-ready. However, there are still some problems.
Each TypeScript file still compiles to a separate JavaScript file. This means that you need to keep updating your HTML file whenever you add new files. When you start creating a complex application, it can become quite a chore to keep this all in sync.
To make it scale, we want a solution that will take all your TypeScript files, and compile it to a single JavaScript file. That way, you can never forget to include JavaScript files in your HTML page, nor do you need to be concerned with the order in which the scripts need to be loaded.
Enter webpack. Simply put, webpack is bundler, which means it can look at your source files, bundle together all the code that needs to be together, and write it to a single output file. It works like this: you point webpack to a single source file, which is the main entry point for your application. Webpack will look at all the import statements in this file, and include those in the output. Next, for each of those imported files, it will again look for all the import statements and include those files in the output as well, etc. etc. This way, webpack builds a tree of files that depend on each other, and bundles these together into a single output file.
Install webpack
The easiest way to install webpack is to use npm. So, let’s first make our folder an actual node project, so that we can use npm. To do this, open your current working folder (the folder that contains your src
and dist
folders) in a command terminal and execute:
> npm init -y
This will create a package.json
file with all default settings for project name, license, etc.
Next, install webpack:
> npm install webpack --save-dev
This installs webpack, and will add it to the list of dependent modules in your package.json
(this way, whenever a new developer downloads the sources and runs npm install
, webpack will be installed on his machine as well).
Install ts-loader
Webpack needs to do one other thing besides simply bundling files: it needs to compile them to JavaScript. Or rather: it needs to tell the TypeScript compiler to do this. To accomplish this, we need to install the ts-loader
module, which is a plugin for webpack (a “loader” in webpack lingo) that enables it to pass TypeScript files to the TypeScript compiler before bundling the output. This can again be done using npm:
> npm install ts-loader --save-dev
Configure webpack
Finally, configure webpack to use this newly installed loader. To do this, create a file called webpack.config.js
and add the following configuration to it:
const path = require('path');
module.exports = {
entry: './src/greeter.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [".tsx", ".ts", ".js"]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
The entry
property tells webpack where to start looking for files and dependencies. It is the ‘root’ of the dependency-tree, if you will.
The module
property instructs webpack to look for all files that end with .ts
or .tsx
and use the ts-loader
loader to process these files.
The resolve
property instructs webpack how to read import statements. For example, if a TypeScript file has the following import statement:
import { Foo } from "./foo"
…then webpack will look for foo.tsx
, foo.ts
and foo.js
in the same folder as the file. If it finds either of those, it will include it in the output bundle.
With the output
property you simply specify where webpack will output its bundled file.
To run webpack and generate your compiled output, simply run:
> webpack
By convention, webpack will look for a file named webpack.config.js
and use it as its configuration file. If you have followed the TypeScript in 5 Minutes tutorial, running webpack will probably generate an error:
Module build failed: Could not load TypeScript. Try installing with
npm install typescript
. If TypeScript is installed globally, try usingnpm link typescript
.
This is because you have installed TypeScript globally, but webpack will only look for a local installation of TypeScript. I prefer to have most of my installations locally, so to fix this error we need to install TypeScript locally:
> npm install typescript --save-dev
This time if you run webpack, everything should work and a nice, single bundle.js
file will be written to the /dist
folder. However, if you inspect this bundle.js
file, you will notice that only greeter.ts
made it to the bundled output, the Student class and Person interface are nowhere to be found. This is because there are no import statements yet, so webpack has no idea that greeter.ts
has dependencies on other files.
Modules and imports
Let’s tell both TypeScript and webpack about the dependencies between our files. The Student class depends on the Person interface, so the first thing we need to do is to export
the interface. This means that the interface will be available for use by other code, such as the Student
class. To do this, simply add the export
keyword to the interface declaration:
export interface Person {
firstName: string;
lastName: string;
}
Now we can import this interface in our student.ts
file:
import { Person } from "./person";
Finally, import both Person and Student into our greeter.ts
file:
import { Student } from "./student";
import { Person } from "./person";
Then run webpack again:
> webpack
And that should do it! webpack finds the greeter.ts
file, as it is configured as its main entry point. Inside that file, it finds two imported files, so it will include all three files in the bundled output.
As your project grows and you add more and more files, all you need to manage are the import statements (and any decent editor can help with that). Webpack and TypeScript will handle the rest for you.
Debugging TypeScript
Although I try to practice Test-Driven Development as much as I can, which reduces the need to debug your code considerably, I still like to use the debugger from time to time to see what’s going on. However, if you open Google Chrome’s debugging console (or that of your favorite browser), all you see is the generated JavaScript, not the nice TypeScript that you wrote. Fortunately, there is a way to tell the browser that the JavaScript code that it executes has related source files. This way is by using source maps. Source maps are files that act as ‘glue’ between the original TypeScript source code and the compiled JavaScript code that gets executed in the browser. They tell the browser which lines of JavaScript code map to which lines of TypeScript code.
So, all we need to do is generate these source maps. Fortunately, this is all built into TypeScript and webpack, all we need to do is enable it.
First, open your tsconfig.js
file and set the "sourceMap"
property to "true"
.
Next, open your webpack configuration file webpack.config.js
and add the devtool
property to the end of the file:
...
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
devtool: "source-map"
};
If you run webpack again and open the page in your browser, you can now see the original TypeScript source files! And what’s even better: you can set breakpoints, inspect variables, and use the debugger just as you would debug plain JavaScript code, right in your original TypeScript sources.
Using external libraries
You’re happily coding along, creating your own new classes and interfaces, but at some point, you decide to use an external JavaScript library. Maybe you still have a need for jQuery, or you want to do complex Date manipulations and you want to use Moment.js. It would be nice if the TypeScript compiler would know about the API of those libraries and do type checking and validation on your code that uses it. Or maybe your editor supports automatic code completion (IntelliSense, for you Microsofties out there) and you want it to know about these external libraries as well.
Again, TypeScript has got your back. There are vast libraries of so called “typings” out there: definition files that describe the APIs of these external libraries, in a way that TypeScript understands.
Installing typings is easy. Again, it’s npm to the rescue. Most of the big libraries have a corresponding type definition in a package called @types/name-of-said-library
.
So, as an example, let’s install Moment.js and its type definitions:
> npm install moment @types/moment --save-dev
Now, there is one catch: let’s say you want to use Moment.js in one of your .ts
files. Of course, you add an import statement such as:
import * as "moment" from "moment";
It will work like a charm, but webpack will now include the entire Moment.js library in your bundle! This is probably not what you want. Usually, you want to create a separate bundle for all your external dependencies or ‘vendor’ files, or maybe you use them directly from a CDN. So, we need to tell webpack that when it encounters an import for “moment”, it should stop digging and not include it in the bundle. The way to do this is by adding the externals
property to the webpack config file:
...
externals: {
moment: "moment"
}
};
To make your HTML work again, just add a script tag and load Moment.js manually:
<script src="node_modules/moment/min/moment.min.js"></script>
Conclusion
There is much, much more to tell about both TypeScript and webpack. This tutorial barely scratches the surface. However, it should provide you with a basic understanding of TypeScript, webpack, and the principles of setting up a front-end build mechanism that will scale with your project.
Of course, this is only one of many ways to go about this, so if you have an alternative approach or find a way the suits your needs better, go for it! And post a comment below, so we can all learn. Happy coding!