Skip to main content

TypeScript Webpack

Introduction

When developing modern web applications with TypeScript, you'll often need a build system to compile your TypeScript code into JavaScript, bundle your modules, and optimize your assets. This is where Webpack comes in. Webpack is a powerful module bundler that can transform, bundle, or package just about any resource or asset for your web application.

In this guide, we'll explore how to set up and configure Webpack to work with TypeScript projects. You'll learn:

  • What Webpack is and why it's useful for TypeScript projects
  • How to set up a basic TypeScript project with Webpack
  • How to configure Webpack for different environments
  • Advanced configurations for optimization

What is Webpack?

Webpack is a static module bundler for modern JavaScript applications. It processes your application and builds a dependency graph that maps every module your project needs, then packages all of those modules into one or more bundles.

When working with TypeScript, Webpack can:

  • Compile TypeScript to JavaScript
  • Bundle multiple files into one
  • Optimize assets for production
  • Handle other assets like CSS, images, and fonts
  • Provide a development server with hot reloading

Setting Up a Basic TypeScript Webpack Project

Let's start by setting up a basic TypeScript project with Webpack.

Step 1: Initialize a project

bash
mkdir typescript-webpack-demo
cd typescript-webpack-demo
npm init -y

Step 2: Install dependencies

bash
# Install TypeScript and Webpack dependencies
npm install --save-dev typescript webpack webpack-cli ts-loader

Step 3: Create TypeScript configuration

Create a tsconfig.json file in your project root:

json
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}

Step 4: Create Webpack configuration

Create a webpack.config.js file in your project root:

javascript
const path = require('path');

module.exports = {
entry: './src/index.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'),
},
};

Step 5: Create a source file

Create a src directory and an index.ts file:

typescript
// src/index.ts
function greeter(person: string): string {
return `Hello, ${person}!`;
}

const user = "TypeScript Developer";
document.body.textContent = greeter(user);

Step 6: Create an index.html

Create a simple HTML file to load your bundled JavaScript:

html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TypeScript Webpack</title>
</head>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>

Step 7: Add npm scripts

Update your package.json to include build scripts:

json
{
"scripts": {
"build": "webpack",
"build:prod": "webpack --mode production",
"watch": "webpack --watch"
}
}

Step 8: Build your project

bash
npm run build

After running this command, Webpack will compile your TypeScript code and output a bundled JavaScript file in the dist directory.

Understanding the Webpack Configuration

Let's examine the key parts of our webpack configuration:

  1. Entry - The entry point of your application, where Webpack starts building its dependency graph.

  2. Module Rules - How different types of modules should be treated:

    • test: /\.tsx?$/ - Apply the rule to .ts and .tsx files
    • use: 'ts-loader' - Use ts-loader to handle these files
    • exclude: /node_modules/ - Don't process TypeScript files in node_modules
  3. Resolve - Configure how modules are resolved:

    • extensions: ['.tsx', '.ts', '.js'] - Allow importing modules without specifying these extensions
  4. Output - Where to emit the bundles:

    • filename: 'bundle.js' - The name of the output bundle
    • path: path.resolve(__dirname, 'dist') - The output directory (absolute path)

Adding Development Server

For a better development experience, let's add webpack-dev-server to our project:

bash
npm install --save-dev webpack-dev-server

Update your webpack.config.js to include dev server configuration:

javascript
module.exports = {
// ... existing config ...

devServer: {
static: './dist',
hot: true,
},
};

Add a new script to your package.json:

json
{
"scripts": {
"start": "webpack serve --open",
"build": "webpack",
"build:prod": "webpack --mode production",
"watch": "webpack --watch"
}
}

Now you can run npm start to start a development server that will automatically reload when you make changes to your code.

Environment-specific Configuration

Let's enhance our setup to handle different environments (development and production):

javascript
const path = require('path');

module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';

return {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
devtool: isProduction ? 'source-map' : 'inline-source-map',
devServer: {
static: './dist',
hot: true,
},
optimization: {
minimize: isProduction,
},
};
};

This configuration includes:

  1. Content hashing for production builds (for cache busting)
  2. Different source map types for development and production
  3. Cleaning the output directory before builds
  4. Minimization only in production

Using Multiple Entry Points

For larger applications, you might want to split your code into multiple bundles:

javascript
module.exports = {
entry: {
main: './src/index.ts',
admin: './src/admin.ts',
},
// ... other config ...
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

This will create two separate bundles: main.bundle.js and admin.bundle.js.

Code Splitting with Dynamic Imports

TypeScript 2.4+ supports dynamic imports, which Webpack can use for code splitting:

typescript
// src/index.ts
async function loadModule() {
const { default: module } = await import('./lazyModule');
module();
}

document.getElementById('loadButton')?.addEventListener('click', loadModule);
typescript
// src/lazyModule.ts
export default function() {
console.log('Lazy module loaded!');
document.body.appendChild(document.createElement('div')).textContent = 'Lazy module loaded!';
}

With this setup, lazyModule.ts will only be loaded when the user clicks the button, reducing the initial bundle size.

Real-world Example: A ToDo Application

Let's put everything together into a more practical example - a simple ToDo application:

Project structure

typescript-webpack-todo/
├── package.json
├── tsconfig.json
├── webpack.config.js
├── src/
│ ├── index.ts
│ ├── todoApp.ts
│ ├── todoItem.ts
│ └── styles.css
└── public/
└── index.html

Webpack configuration with CSS support

javascript
const path = require('path');

module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 9000,
},
};

Don't forget to install the required loaders:

bash
npm install --save-dev css-loader style-loader

TypeScript implementation

typescript
// src/todoItem.ts
export interface TodoItem {
id: number;
text: string;
completed: boolean;
}

// src/todoApp.ts
import { TodoItem } from './todoItem';

export class TodoApp {
private todos: TodoItem[] = [];
private nextId: number = 1;
private todoList: HTMLUListElement;
private input: HTMLInputElement;

constructor() {
this.todoList = document.getElementById('todo-list') as HTMLUListElement;
this.input = document.getElementById('new-todo') as HTMLInputElement;

const addButton = document.getElementById('add-todo') as HTMLButtonElement;
addButton.addEventListener('click', () => this.addTodo());

this.input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.addTodo();
}
});
}

addTodo(): void {
const text = this.input.value.trim();
if (text) {
const todo: TodoItem = {
id: this.nextId++,
text,
completed: false
};

this.todos.push(todo);
this.renderTodo(todo);
this.input.value = '';
}
}

toggleTodo(id: number): void {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;

const element = document.getElementById(`todo-${id}`);
if (element) {
if (todo.completed) {
element.classList.add('completed');
} else {
element.classList.remove('completed');
}
}
}
}

renderTodo(todo: TodoItem): void {
const li = document.createElement('li');
li.id = `todo-${todo.id}`;
li.className = todo.completed ? 'todo-item completed' : 'todo-item';

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = todo.completed;
checkbox.addEventListener('change', () => this.toggleTodo(todo.id));

const span = document.createElement('span');
span.textContent = todo.text;

li.appendChild(checkbox);
li.appendChild(span);
this.todoList.appendChild(li);
}
}

// src/index.ts
import { TodoApp } from './todoApp';
import './styles.css';

document.addEventListener('DOMContentLoaded', () => {
new TodoApp();
});
css
/* src/styles.css */
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 20px;
}

h1 {
color: #333;
}

.todo-container {
margin-top: 20px;
}

.add-todo {
display: flex;
margin-bottom: 20px;
}

.add-todo input {
flex: 1;
padding: 8px;
font-size: 16px;
}

.add-todo button {
padding: 8px 16px;
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
}

.todo-list {
list-style-type: none;
padding: 0;
}

.todo-item {
padding: 10px;
border-bottom: 1px solid #ddd;
display: flex;
align-items: center;
}

.todo-item input {
margin-right: 10px;
}

.completed span {
text-decoration: line-through;
color: #888;
}

HTML file

html
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TypeScript Todo App</title>
</head>
<body>
<h1>TypeScript Todo App</h1>
<div class="todo-container">
<div class="add-todo">
<input type="text" id="new-todo" placeholder="Add a new task..." />
<button id="add-todo">Add</button>
</div>
<ul id="todo-list" class="todo-list"></ul>
</div>
<script src="bundle.js"></script>
</body>
</html>

With this setup, you can run npm start to launch the Todo application in development mode, or npm run build:prod to create a production build.

Visualizing the Bundle

Understanding what's in your bundle is important for optimization. You can use the Webpack Bundle Analyzer plugin:

bash
npm install --save-dev webpack-bundle-analyzer

Add it to your webpack config:

javascript
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
// ... existing config ...
plugins: [
new BundleAnalyzerPlugin()
]
};

Now when you run npm run build, it will generate a visual representation of your bundle sizes.

Summary

In this guide, we've covered how to integrate Webpack with TypeScript to create a modern web development workflow:

  1. Setting up a basic TypeScript and Webpack project
  2. Understanding Webpack configuration
  3. Adding a development server for faster development
  4. Creating environment-specific configurations
  5. Using code splitting for better performance
  6. Building a real-world ToDo application

Webpack provides a powerful toolset for managing TypeScript projects, from simple applications to complex enterprise systems. By understanding how to configure Webpack properly, you can optimize your development workflow and build process.

Additional Resources

Exercises

  1. Extend the ToDo application to save todos in localStorage so they persist across page reloads.
  2. Add a filter to show all, active, or completed todos.
  3. Configure Webpack to use different CSS preprocessors like SCSS or LESS.
  4. Implement code splitting to lazy load a settings panel for the ToDo application.
  5. Add unit tests for your TypeScript code and configure Webpack to run them.

By mastering TypeScript and Webpack together, you'll have a powerful toolkit for building modern web applications that are both type-safe and optimized for production.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)