The Only TypeScript Article You'd Ever Need - 4/4
Taking TS From Code to Production
Welcome to the final part of the TypeScript series. If you’re here after reading them, congratulations. You’ve survived the worst.
But here’s the uncomfortable truth: all that beautiful, type-safe TypeScript you’ve been writing? It’s worthless without a way to make your TS files understandable for browsers. Luckily, there are tools for this, but imagine if there weren’t any. You would just have dozens of .ts files scattered across your project.
Now, how exactly do you get all of this into a browser? How do you turn your collection of TS modules into something that can actually run on the web? How do you ensure that your code doesn’t just work on your machine, but works consistently across your entire team?
Welcome to the final lesson.
The Bundle Problem
As you already know, browsers don’t understand TypeScript. They don’t understand your import statements. A browser sees your code and says, “What is this import { User } from './types/User' nonsense? I want HTML, CSS, and JS. That’s it.”
In the early days of the web, developers had to manually list every JS file in their HTML:
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/user.js"></script>
<script src="js/componentA.js"></script>
<script src="js/componentB.js"></script>
<!-- ...and 47 more files -->
This approach was a nightmare for three reasons:
Performance Death by a Thousand Cuts: Every
<script>tag is a separate HTTP request. Your user’s browser has to requestutils.js, wait for the response, download it, parse it, then move on toapi.js. For a modern app with dozens of files, users would have enough time to question their life choices before your app became interactive.Dependency Hell: Order matters. If
componentA.jsneeds functions fromutils.js, you better loadutils.jsfirst. Miss the order? Your app crashes with cryptic errors about undefined functions. Managing this manually across a team is…yeah.Global Scope Pollution: Before modules were standardized, every file dumped its variables into the global
windowobject. Two files defining a variable nameduser? Congratulations, you’ve created a bug that will take hours to debug.
The solution was obvious: we needed tools that could intelligently combine our modular source code into optimized, browser-ready bundles. We needed bundlers.
Webpack
Webpack is the OG bundler that powered the web development revolution. It’s complex, sometimes intimidating, and has a config file that can grow to hundreds of lines. It’s also incredibly powerful and the reason you can write modular JavaScript with confidence.
Think of Webpack as a detective and a compiler rolled into one. It starts at your application’s entry point, follows every import statement like breadcrumbs, builds a complete map of your dependency tree, and then intelligently combines everything into optimized bundles.
The Four Pillars of Webpack
Understanding Webpack means understanding four core concepts:
Entry
The entry point is where Webpack starts its investigation. It’s patient zero of your dependency graph:
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.ts", // Start here and follow every import
};
From this single file, Webpack will discover and bundle your entire application.
Output
The output configuration tells Webpack where to put the finished product:
module.exports = {
entry: "./src/index.ts",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
clean: true, // Clear the dist folder before each build
},
};
This creates a single bundle.js file containing all your application code, ready for the browser.
Loaders
Here’s the kicker: Webpack only understands JavaScript and JSON. Encounter a .ts file? Panic. See import './styles.css'? Complete meltdown.
Loaders teach Webpack how to process other file types and transform them into valid JavaScript modules:
module.exports = {
entry: "./src/index.ts",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.tsx?$/, // Files ending in .ts or .tsx
use: "ts-loader", // Process them with ts-loader
exclude: /node_modules/, // Skip dependencies (they're already compiled)
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"], // Look for these file types
},
};
Now when Webpack encounters a .ts file, it hands it to ts-loader, which uses your tsconfig.json to transform it into JavaScript.
lugins
While loaders work on individual files, plugins operate on the entire build process. They can optimize output, generate HTML files, and perform complex transformations:
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// ...previous config
plugins: [
new HtmlWebpackPlugin({
title: "My TypeScript App",
template: "src/index.html", // Optional: use your own template
}),
],
};
This plugin automatically generates an index.html file in your dist folder with the correct <script> tags pointing to your bundles. No more manual HTML management.
The Complete Professional Setup
Here’s a basic Webpack config example that handles TS, development servers, and optimization:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = (env, argv) => {
const isDevelopment = argv.mode === "development";
return {
mode: argv.mode || "development",
entry: "./src/index.ts",
output: {
filename: isDevelopment ? "[name].js" : "[name].[contenthash].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
plugins: [
new HtmlWebpackPlugin({
title: "Professional TypeScript App",
template: "src/index.html",
}),
],
devServer: {
static: "./dist",
hot: true, // Hot module replacement for development
},
devtool: isDevelopment ? "inline-source-map" : "source-map",
};
};
This config handles development and production builds, includes CSS processing, sets up a development server with hot reloading, and generates source maps for debugging.
Vite
Webpack is powerful, but it has one major weakness: it’s slow. For large applications, starting a development server can take minutes because Webpack builds your entire application upfront.
Vite (pronounced “veet,” French for “fast”) looked at this problem and said, “What if we were smarter about this?” And they were.
The Vite Revolution
Vite leverages native ES modules in modern browsers to create a fundamentally different development experience:
- Instant Server Startup: Vite starts a development server in milliseconds
- On-Demand Compilation: It only transpiles files when the browser requests them
- Lightning-Fast HMR: Changes appear in the browser almost instantly
Here’s what happens when you request a page in Vite:
- Browser requests
index.html - Vite serves it with
<script type="module" src="/src/main.ts"></script> - Browser requests
/src/main.ts - Vite transpiles
main.tson-the-fly and serves the JavaScript - If
main.tsimports other files, the process repeats for each import
This lazy, on-demand approach makes development incredibly fast because Vite only does work when you actually need it.
The Production Reality Check
This on-demand strategy is perfect for development but terrible for production (remember the network waterfall problem?). Vite knows this, so for production builds, it uses Rollup under the hood to create traditional, optimized bundles.
Getting started with Vite is refreshingly simple:
npm create vite@latest my-app -- --template vanilla-ts
cd my-app
npm install
npm run dev
That’s it. You get a complete TS setup with hot module replacement, CSS processing, and optimized production builds out of the box.
Webpack vs Vite: The Practical Choice
- Choose Webpack when: You need maximum configurability, have complex build requirements, or are working on large enterprise applications with unique needs
- Choose Vite when: You want fast development, are building modern SPAs, or prioritize developer experience
Both are excellent tools. The best choice depends on your specific requirements and team preferences.
Code Quality Gatekeepers
You’ve got your bundler configured, your TS compiling, and your modules loading. Congratulations, you’re now capable of shipping bugs to production at great scale and speed.
Now, the TS compiler catches type errors, which is good, but not great. Because it doesn’t catch bad code. It won’t stop you from declaring variables you never use, mixing var and const randomly, or writing functions that are 200 lines long. For that, you need different tools: linters and formatters.
ESLint
ESLint is a static analysis tool that examines your code for potential problems without running it. Think of it as the colleague who reviews your code and points out every questionable decision, except it never gets tired, never misses anything, and never feels bad about hurting your feelings.
Setting up ESLint for TS requires three packages working together:
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
eslint: The core linting engine@typescript-eslint/parser: Teaches ESLint how to read TypeScript syntax@typescript-eslint/eslint-plugin: Provides TypeScript-specific rules
Configure it with .eslintrc.js:
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "@typescript-eslint/recommended"],
root: true,
rules: {
// Customize rules to match your team's preferences
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"prefer-const": "error",
},
};
Now ESLint will catch issues like:
- Unused variables: That
temporaryDebugVariableyou forgot to remove - Inconsistent declarations: Mixing
letandvarrandomly - Type safety violations: Using
anywhen you should be more specific - Logic errors: Assignment in conditionals (
if (x = 5)instead ofif (x === 5))
Prettier
While ESLint focuses on code quality, Prettier handles code formatting. Its way of working is pretty simple: “There is one correct way to format code. I will enforce it.”
This authoritarian approach to formatting is actually liberating. It eliminates all team debates about semicolons, quotes, indentation, yada yada.
Install and configure:
npm install -D prettier
Create .prettierrc.json:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
Set up your editor to format on save, and you’ll never think about code formatting again.
Making Peace Between ESLint and Prettier
ESLint and Prettier can conflict if ESLint tries to enforce formatting rules that Prettier will override. The solution is eslint-config-prettier, which disables all ESLint formatting rules:
npm install -D eslint-config-prettier
Update your ESLint config:
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"@typescript-eslint/recommended",
"prettier", // Must be last to override conflicting rules
],
root: true,
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
},
};
Now ESLint handles code quality, Prettier handles formatting, and they work in harmony.
Automation
The real power comes from automation. Add these scripts to your package.json:
{
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format": "prettier --write src",
"build": "webpack --mode production",
"dev": "webpack serve --mode development"
}
}
Now you can:
npm run lint- Check for code quality issuesnpm run lint:fix- Automatically fix what can be fixednpm run format- Format all code consistentlynpm run build- Create production bundlesnpm run dev- Start development server
(replace with pnpm (my personal recommendation) or yarn, depending on what you use)
For max automation, set up your editor to run these tools on save, and configure git hooks to run them before commits. Your code quality will be enforced automatically, without anyone having to remember to do it.
The Workflow
This is a basic workflow for how these tools would work together in real projects:
# 1. Create a new project
mkdir professional-ts-app
cd professional-ts-app
npm init -y
# 2. Install TypeScript and build tools
npm install -D typescript webpack webpack-cli webpack-dev-server
npm install -D ts-loader html-webpack-plugin
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install -D prettier eslint-config-prettier
# 3. Create configuration files
# (webpack.config.js, tsconfig.json, .eslintrc.js, .prettierrc.json)
# 4. Set up package.json scripts
# 5. Configure your editor for format-on-save and lint-on-save
# 6. Start developing with confidence
Your development process now looks like:
- Write TS code with full type safety
- Save files - Editor automatically formats and shows linting errors
- Run
npm run dev- Webpack serves your app with hot reloading - Commit changes - Git hooks ensure code quality
- Deploy -
npm run buildcreates optimized production bundles
You can configure this as per your requirement, or as your project evolves and changes are needed.
What’s Next in Your Journey
If you’ve been following along properly and with proper practice on the explained topics, go and convert your big project that you built on the last part of the JavaScript article, now that you’ve learned to think systematically about code quality, maintainability, and developer experience.
Practice working with the concepts you’ve learned here (bundling, linting, formatting, automated workflows), because they will apply far beyond TypeScript. Whether you’re working with React, Vue, Node.js, or any modern JavaScript framework, these principles will serve you well.
You’ve built the complete foundation, now go build something amazing on top of it.
For those of you who’ve been here from the very first part of this series, I love you <3
See you, learner :)