This blog post describes how to create web apps via TypeScript and webpack. We will only be using the DOM API, not a particular frontend framework. The repository ts-demo-webpack
with the files can be downloaded from GitHub.
Required knowledge: It helps if you have a rough understanding of how TypeScript, webpack, and npm work.
Using ES modules via TypeScript and npm is still fragile. Therefore, we will stick with CommonJS modules, bundled as script files.
ts-demo-webpack
This is how the repository ts-demo-webpack
is structured:
ts-demo-webpack/
build/ (created on demand)
html/
index.html
package.json
ts/
src/
main.ts
tsconfig.json
webpack.config.js
In order to build the web app, we need to compile two sets of files into the directory build/
:
ts/
.html/
.Both tasks are handled by webpack:
For TypeScript, webpack starts at main.ts
, locates all TypeScript and JavaScript files that are used, and compiles them into the single script file build/main-bundle.js
. This process is called bundling. For compiling TypeScript to JavaScript, webpack uses the loader (plugin) ts-loader
.
Copying the files in html/
is done via the webpack plugin copy-webpack-plugin
.
First we need to install all npm packages that our web app depends on:
npm install
Then we need to run webpack (which was also installed by the previous step) via a script in package.json
:
npm run wpw
From now on, webpack watches the files in the repository for changes and rebuilds the web app whenever it detects any.
In a different command line, we can now start a web server that serves the contents of build/
on localhost:
npm run serve
If we go to the URL printed out by the web server, we can see the web app in action.
Note that simple reloading may not be enough to see the results after changes – due to caching. You may have to force-reload by pressing shift when reloading.
Instead of building from a command line, we can also do that from within Visual Studio Code, via a so-called build task:
.vscode/tasks.json
:"problemMatcher": ["$tsc-watch"],
We can now execute “Run Build Task...” from the “Terminal” menu.
package.json
package.json
specifies our scripts and the npm packages that the project depends on:
{
"private": true,
"scripts": {
"tsc": "tsc",
"tscw": "tsc --watch",
"wp": "webpack",
"wpw": "webpack --watch",
"serve": "http-server build"
},
"dependencies": {
"@types/lodash": "···",
"copy-webpack-plugin": "···",
"http-server": "···",
"lodash": "···",
"ts-loader": "···",
"typescript": "···",
"webpack": "···",
"webpack-cli": "···"
}
}
"private": true
means that npm doesn’t complain if we don’t provide a package name and a package version.tsc, tscw
: We probably won’t invoke the TypeScript compiler tsc
directly if we use webpack with ts-loader
.wp
: run webpack once, compile everything.wpw
: webpack watches the files and only compiles what changes, incrementally.serve
: run the server http-server
and serve the contents of directory build/
.webpack
, webpack-cli
, ts-loader
, copy-webpack-plugin
ts-loader
: typescript
http-server
lodash
, @types/lodash
webpack.config.js
This is how we configure webpack:
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
mode: "development",
devtool: "inline-source-map",
entry: {
main: "./ts/src/main.ts",
},
output: {
path: path.resolve(__dirname, 'build'),
filename: "[name]-bundle.js",
},
resolve: {
// Add ".ts" and ".tsx" as resolvable extensions.
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
// all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
{ test: /\.tsx?$/, loader: "ts-loader" },
],
},
plugins: [
new CopyWebpackPlugin([
{
from: './html',
}
]),
],
};
For more information on configuring webpack, see the webpack website.
tsconfig.json
This file configures the TypeScript compiler:
{
"compilerOptions": {
"rootDir": "ts",
"outDir": "dist",
"target": "es2019",
"lib": [
"es2019",
"dom"
],
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"sourceMap": true
}
}
The option outDir
is not needed if we use webpack with ts-loader
. However, we’ll need it if we use webpack without a loader (as explained later in this post).
index.html
This is the HTML page of the web app:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>ts-demo-webpack</title>
</head>
<body>
<div id="output"></div>
<script src="main-bundle.js"></script>
</body>
</html>
The <div>
with the id output
is where the web app displays its output. main-bundle.js
contains the bundled code.
main.ts
This is the TypeScript code of the web app:
import template from 'lodash/template';
const outputElement = document.getElementById('output');
if (outputElement) {
var compiled = template(`
<h1><%- heading %></h1>
Current date and time: <%- dateTimeString %>
`.trim());
outputElement.innerHTML = compiled({
heading: 'ts-demo-webpack',
dateTimeString: new Date().toISOString(),
});
}
For more information on template()
, see Lodash’s documentation.
webpack-no-loader.config.js
Instead of depending on ts-loader
, we can also first compile all TypeScript files to JavaScript files (via the TypeScript compiler) and then bundle those files via webpack. More information on how that works is provided in the blog post “Creating CommonJS-based npm packages via TypeScript”.
We now don’t have to configure ts-loader
and our webpack configuration file is simpler:
const path = require('path');
module.exports = {
entry: {
main: "./dist/src/main.js",
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name]-bundle.js',
},
plugins: [
new CopyWebpackPlugin([
{
from: './html',
}
]),
],
};
Why would we want to produce intermediate files before bundling them? One benefit is that we can use Node.js to run unit tests for some of the TypeScript code.