Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand source maps fundamentals ๐ฏ
- Apply source maps in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Source Maps in TypeScript! ๐ In this guide, weโll explore the magical world of debugging compiled TypeScript code using source maps.
Youโll discover how source maps can transform your TypeScript debugging experience. Whether youโre building web applications ๐, server-side code ๐ฅ๏ธ, or libraries ๐, understanding source maps is essential for effective debugging and maintaining code quality.
By the end of this tutorial, youโll feel confident setting up and using source maps in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Source Maps
๐ค What are Source Maps?
Source maps are like a GPS for your code! ๐บ๏ธ Think of them as a translator that helps your browser understand where your original TypeScript code is when youโre debugging the compiled JavaScript.
In TypeScript terms, source maps are special files that map compiled JavaScript back to your original TypeScript source code. This means you can:
- โจ Debug TypeScript code directly in the browser
- ๐ Set breakpoints in your original source files
- ๐ก๏ธ See meaningful stack traces with TypeScript file references
๐ก Why Use Source Maps?
Hereโs why developers love source maps:
- Direct TypeScript Debugging ๐: Debug your original code, not compiled JS
- Better Error Messages ๐ป: Stack traces point to your TypeScript files
- Improved Development Experience ๐: Work with code you actually wrote
- Production Debugging ๐ง: Optionally debug production issues
Real-world example: Imagine building a shopping cart ๐. With source maps, when an error occurs, youโll see it points to line 45 in ShoppingCart.ts
, not line 1247 in the minified JavaScript bundle!
๐ง Basic Syntax and Usage
๐ Simple Configuration
Letโs start with a friendly example:
// ๐ tsconfig.json - Basic source map setup
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"sourceMap": true, // ๐ฏ Enable source maps!
"outDir": "./dist", // ๐ Output directory
"rootDir": "./src", // ๐ Source directory
"strict": true
}
}
๐ก Explanation: The sourceMap: true
option tells TypeScript to generate .map
files alongside your compiled JavaScript. These files contain the mapping information!
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Development configuration
{
"compilerOptions": {
"sourceMap": true, // ๐ For debugging
"declarationMap": true // ๐จ For .d.ts files too
}
}
// ๐ Pattern 2: Production-ready setup
{
"compilerOptions": {
"sourceMap": false, // โ Disabled for production
"inlineSourceMap": false, // โ No inline maps
"inlineSources": false // โ Don't embed source code
}
}
// ๐ Pattern 3: Advanced debugging setup
{
"compilerOptions": {
"sourceMap": true,
"inlineSourceMap": false, // ๐ฏ Separate .map files
"inlineSources": true, // โจ Include source in maps
"sourceRoot": "/src" // ๐ Source root for debuggers
}
}
๐ก Practical Examples
๐ Example 1: E-commerce Cart with Debugging
Letโs build something real with proper source map configuration:
// ๐ src/cart/ShoppingCart.ts
interface Product {
id: string;
name: string;
price: number;
emoji: string; // Every product needs an emoji!
}
class ShoppingCart {
private items: Product[] = [];
// โ Add item to cart
addItem(product: Product): void {
this.items.push(product);
console.log(`Added ${product.emoji} ${product.name} to cart!`);
// ๐ Intentional bug for debugging demo
if (product.price < 0) {
throw new Error(`๐ซ Invalid price for ${product.name}`);
}
}
// ๐ฐ Calculate total with debugging support
getTotal(): number {
console.log("๐งฎ Calculating total...");
debugger; // ๐ This will stop in TypeScript, not JS!
return this.items.reduce((sum, item) => {
console.log(`๐ฐ Adding ${item.name}: $${item.price}`);
return sum + item.price;
}, 0);
}
// ๐ List items with error handling
listItems(): void {
try {
console.log("๐ Your cart contains:");
this.items.forEach((item, index) => {
if (!item.emoji) {
throw new Error(`๐ฑ Missing emoji for item ${index}`);
}
console.log(` ${item.emoji} ${item.name} - $${item.price}`);
});
} catch (error) {
// ๐ฏ With source maps, this error will point to TypeScript!
console.error("Cart listing failed:", error);
}
}
}
// ๐ฎ Let's use it with debugging!
const cart = new ShoppingCart();
cart.addItem({ id: "1", name: "TypeScript Book", price: 29.99, emoji: "๐" });
cart.addItem({ id: "2", name: "Coffee", price: 4.99, emoji: "โ" });
๐ฏ Configuration for this example:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"inlineSources": true
}
}
๐ฎ Example 2: Game Debug System
Letโs make debugging fun with a game system:
// ๐ src/game/GameEngine.ts
interface Player {
id: string;
name: string;
level: number;
health: number;
emoji: string;
}
class GameEngine {
private players: Map<string, Player> = new Map();
private debugMode: boolean = true;
// ๐ฎ Add player with debug logging
addPlayer(player: Player): void {
if (this.debugMode) {
console.log(`๐ฏ Adding player: ${player.emoji} ${player.name}`);
debugger; // ๐ Debug breakpoint in TypeScript!
}
this.players.set(player.id, player);
this.logGameState();
}
// โ๏ธ Battle system with error tracking
battle(playerId1: string, playerId2: string): void {
const player1 = this.players.get(playerId1);
const player2 = this.players.get(playerId2);
if (!player1 || !player2) {
// ๐ฏ Source maps will show this error in TypeScript!
throw new Error("๐ซ One or both players not found!");
}
console.log(`โ๏ธ Battle: ${player1.emoji} vs ${player2.emoji}`);
// ๐ฒ Simple battle logic with debugging
const damage = Math.floor(Math.random() * 20) + 1;
player2.health -= damage;
if (this.debugMode) {
console.log(`๐ฅ ${player1.name} deals ${damage} damage!`);
console.log(`โค๏ธ ${player2.name} health: ${player2.health}`);
}
if (player2.health <= 0) {
console.log(`๐ ${player1.name} wins!`);
this.players.delete(player2.id);
}
}
// ๐ Debug helper with detailed logging
private logGameState(): void {
if (!this.debugMode) return;
console.log("๐ฎ Game State Debug:");
console.log(`๐ฅ Players: ${this.players.size}`);
for (const [id, player] of this.players) {
console.log(` ${player.emoji} ${player.name} (Level ${player.level}, HP: ${player.health})`);
}
}
// ๐ง Toggle debug mode
setDebugMode(enabled: boolean): void {
this.debugMode = enabled;
console.log(`๐ ๏ธ Debug mode: ${enabled ? 'ON' : 'OFF'}`);
}
}
// ๐ฏ Usage with debugging
const game = new GameEngine();
game.addPlayer({
id: "1",
name: "Sarah the Brave",
level: 5,
health: 100,
emoji: "โ๏ธ"
});
game.addPlayer({
id: "2",
name: "Dragon Lord",
level: 8,
health: 150,
emoji: "๐"
});
// ๐ฅ Start battle (will trigger debug breakpoints!)
game.battle("1", "2");
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Webpack Integration
When youโre ready to level up with bundlers:
// ๐ webpack.config.js
module.exports = {
mode: 'development',
entry: './src/index.ts',
devtool: 'source-map', // ๐ฏ Generate separate .map files
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
sourceMapFilename: '[name].js.map' // ๐บ๏ธ Custom map naming
},
};
// ๐จ Alternative devtool options:
const devtoolOptions = {
'source-map': '๐ฏ Separate .map files (best quality)',
'inline-source-map': '๐พ Embedded in bundle (larger size)',
'eval-source-map': 'โก Fast rebuilds (dev only)',
'cheap-module-source-map': '๐ Faster builds (less detail)'
};
๐๏ธ Advanced Topic 2: Production Source Maps
For the brave developers who want production debugging:
// ๐ Production configuration with conditional source maps
interface BuildConfig {
environment: 'development' | 'staging' | 'production';
enableSourceMaps: boolean;
uploadToSentry: boolean;
}
const buildConfigs: Record<string, BuildConfig> = {
development: {
environment: 'development',
enableSourceMaps: true, // โ
Always on for dev
uploadToSentry: false
},
staging: {
environment: 'staging',
enableSourceMaps: true, // โ
Helpful for testing
uploadToSentry: true // ๐ Upload for error tracking
},
production: {
environment: 'production',
enableSourceMaps: false, // โ Usually off for security
uploadToSentry: true // ๐ฏ Error tracking only
}
};
// ๐ Dynamic tsconfig generation
function generateTsConfig(config: BuildConfig) {
return {
compilerOptions: {
target: "ES2020",
module: "commonjs",
sourceMap: config.enableSourceMaps,
inlineSourceMap: false, // ๐ฏ Never inline in production
removeComments: config.environment === 'production',
outDir: "./dist",
declaration: true,
declarationMap: config.enableSourceMaps
}
};
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Source Maps in Production
// โ Wrong way - exposing source maps publicly!
{
"compilerOptions": {
"sourceMap": true, // ๐ฅ Exposes source code to users!
"inlineSources": true // ๐ฅ Even worse - embedded source!
}
}
// โ
Correct way - conditional source maps!
{
"compilerOptions": {
"sourceMap": process.env.NODE_ENV !== 'production',
"inlineSources": false, // ๐ก๏ธ Never expose source code
"removeComments": true // ๐งน Clean production code
}
}
๐คฏ Pitfall 2: Wrong Source Root Configuration
// โ Dangerous - incorrect source mapping!
{
"compilerOptions": {
"sourceMap": true,
"sourceRoot": "/wrong/path" // ๐ฅ Debugger can't find files!
}
}
// โ
Safe - proper source root setup!
{
"compilerOptions": {
"sourceMap": true,
"sourceRoot": "/", // ๐ฏ Relative to project root
"mapRoot": "/maps", // ๐บ๏ธ Where to find .map files
"rootDir": "./src" // ๐ Source code location
}
}
๐ Pitfall 3: Security Concerns
// โ Security risk - exposing too much!
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true, // ๐ฅ Source code in map files!
"sourceRoot": "file:///absolute/path" // ๐ฅ Exposes file system!
}
}
// โ
Secure configuration!
{
"compilerOptions": {
"sourceMap": process.env.NODE_ENV === 'development',
"inlineSources": false, // ๐ก๏ธ Keep source code private
"sourceRoot": "", // ๐ฏ Relative paths only
"removeComments": true // ๐งน Remove development comments
}
}
๐ ๏ธ Best Practices
- ๐ฏ Environment-Specific: Only enable source maps where needed
- ๐ Never Inline in Production: Keep source maps separate
- ๐ก๏ธ Security First: Donโt expose source code unnecessarily
- ๐จ Use Proper Tool Settings: Configure your bundler correctly
- โจ Monitor Performance: Source maps can increase bundle size
๐งช Hands-On Exercise
๐ฏ Challenge: Debug-Ready TypeScript Application
Create a debugging-friendly TypeScript application with proper source map configuration:
๐ Requirements:
- โ Multi-file TypeScript project with classes and interfaces
- ๐ท๏ธ Proper source map configuration for development and production
- ๐ค Error handling with meaningful stack traces
- ๐ Debug logging system with toggleable verbosity
- ๐จ Integration with a bundler (webpack or similar)
๐ Bonus Points:
- Add conditional source map generation
- Implement error tracking integration
- Create a debug dashboard component
- Set up automated source map testing
๐ก Solution
๐ Click to see solution
// ๐ src/logger/DebugLogger.ts
interface LogLevel {
level: 'debug' | 'info' | 'warn' | 'error';
emoji: string;
color: string;
}
const LOG_LEVELS: Record<string, LogLevel> = {
debug: { level: 'debug', emoji: '๐', color: '#666' },
info: { level: 'info', emoji: 'โน๏ธ', color: '#007acc' },
warn: { level: 'warn', emoji: 'โ ๏ธ', color: '#ff9800' },
error: { level: 'error', emoji: '๐ซ', color: '#f44336' }
};
export class DebugLogger {
private enabled: boolean = true;
private minLevel: string = 'debug';
constructor(enabled: boolean = true) {
this.enabled = enabled;
}
// ๐ฏ Smart logging with source map support
log(level: keyof typeof LOG_LEVELS, message: string, data?: any): void {
if (!this.enabled) return;
const logLevel = LOG_LEVELS[level];
const timestamp = new Date().toISOString();
// ๐ Get stack trace for source mapping
const stack = new Error().stack;
const caller = this.parseStackTrace(stack);
console.log(
`${logLevel.emoji} [${timestamp}] ${level.toUpperCase()}: ${message}`,
data ? data : '',
caller ? `\n๐ ${caller}` : ''
);
// ๐ฏ Break on errors in debug mode
if (level === 'error' && this.enabled) {
debugger; // This will stop in TypeScript with source maps!
}
}
// ๐ Parse stack trace to show TypeScript file info
private parseStackTrace(stack?: string): string | null {
if (!stack) return null;
const lines = stack.split('\n');
// Skip the first two lines (Error and this method)
const callerLine = lines[3];
// Extract file info from stack trace
const match = callerLine?.match(/at.*\((.+):(\d+):(\d+)\)/);
if (match) {
const [, file, line, col] = match;
return `${file.split('/').pop()}:${line}:${col}`;
}
return null;
}
// ๐ ๏ธ Convenience methods
debug(message: string, data?: any): void {
this.log('debug', message, data);
}
info(message: string, data?: any): void {
this.log('info', message, data);
}
warn(message: string, data?: any): void {
this.log('warn', message, data);
}
error(message: string, data?: any): void {
this.log('error', message, data);
}
}
// ๐ src/app/Application.ts
import { DebugLogger } from '../logger/DebugLogger';
interface User {
id: string;
name: string;
email: string;
emoji: string;
}
export class Application {
private logger: DebugLogger;
private users: Map<string, User> = new Map();
constructor() {
this.logger = new DebugLogger(process.env.NODE_ENV === 'development');
this.logger.info("๐ Application starting up!");
}
// ๐ค Add user with debugging
addUser(user: User): void {
try {
this.logger.debug("Adding new user", { user });
if (!user.email.includes('@')) {
throw new Error(`Invalid email for user: ${user.name}`);
}
this.users.set(user.id, user);
this.logger.info(`โ
User added: ${user.emoji} ${user.name}`);
} catch (error) {
// ๐ฏ Source maps will show TypeScript file and line!
this.logger.error("Failed to add user", { user, error });
throw error;
}
}
// ๐ Get user with error handling
getUser(id: string): User | null {
this.logger.debug(`Looking up user: ${id}`);
const user = this.users.get(id);
if (!user) {
this.logger.warn(`User not found: ${id}`);
return null;
}
this.logger.debug(`Found user: ${user.emoji} ${user.name}`);
return user;
}
// ๐ Get application stats
getStats(): void {
this.logger.info("๐ Application Statistics:");
this.logger.info(`๐ฅ Total users: ${this.users.size}`);
for (const [id, user] of this.users) {
this.logger.debug(` ${user.emoji} ${user.name} (${user.email})`);
}
}
}
// ๐ tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"inlineSourceMap": false,
"inlineSources": false,
"sourceRoot": "/src",
"declaration": true,
"declarationMap": true,
"removeComments": false,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
// ๐ webpack.config.js
const path = require('path');
module.exports = (env, argv) => {
const isDevelopment = argv.mode === 'development';
return {
entry: './src/index.ts',
// ๐ฏ Dynamic source map configuration
devtool: isDevelopment ? 'eval-source-map' : false,
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
configFile: 'tsconfig.json'
}
},
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
sourceMapFilename: '[name].js.map'
},
// ๐ง Development server with source map support
devServer: {
static: './dist',
port: 3000,
hot: true
}
};
};
// ๐ src/index.ts
import { Application } from './app/Application';
const app = new Application();
// ๐ฎ Test the application with debugging
try {
app.addUser({
id: '1',
name: 'Alice',
email: '[email protected]',
emoji: '๐ฉโ๐ป'
});
app.addUser({
id: '2',
name: 'Bob',
email: 'invalid-email', // This will trigger an error!
emoji: '๐จโ๐ป'
});
} catch (error) {
console.error('Application error:', error);
}
app.getStats();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Configure source maps with confidence ๐ช
- โ Debug TypeScript code directly in browsers and IDEs ๐ก๏ธ
- โ Set up environment-specific builds for different stages ๐ฏ
- โ Handle security considerations when deploying ๐
- โ Integrate with modern bundlers and build tools! ๐
Remember: Source maps are your debugging superpowers! They bridge the gap between what you write and what runs. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered source maps in TypeScript!
Hereโs what to do next:
- ๐ป Practice with the debugging exercise above
- ๐๏ธ Set up source maps in your current project
- ๐ Move on to our next tutorial: โTypeScript with Webpack: Advanced Bundlingโ
- ๐ Share your debugging wins with the community!
Remember: Every TypeScript expert was once a beginner who learned to debug effectively. Keep coding, keep learning, and most importantly, debug with confidence! ๐
Happy debugging! ๐๐โจ