Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices

The Developer’s Dilemma

As a TypeScript developer, I’ve always appreciated the robustness and type safety that TypeScript brings to my projects. Recently, I wanted to build a Chrome extension but found most tutorials were JavaScript-based. W…


This content originally appeared on DEV Community and was authored by Cendekia

The Developer's Dilemma

As a TypeScript developer, I've always appreciated the robustness and type safety that TypeScript brings to my projects. Recently, I wanted to build a Chrome extension but found most tutorials were JavaScript-based. While JavaScript is great, I wanted to leverage TypeScript's features to create a more maintainable and scalable extension.

Project Overview

We'll build a Meme Roulette extension that fetches and displays random images from Imgur. This practical example will demonstrate TypeScript integration with Chrome extensions while creating something fun and useful.

Setting Up the Project

1. Initialize the Project

mkdir meme-roulette-ts
cd meme-roulette-ts
npm init -y

First, let's create our project structure:

meme-roulette-ts/
├── src/
│   ├── background/
│   │   └── background.ts
│   ├── popup/
│   │   ├── popup.html
│   │   └── popup.ts
│   ├── content/
│   │   └── content.ts
│   └── types/
│       └── imgur.ts
├── package.json
├── tsconfig.json
├── webpack.config.js
└── manifest.json

Install dependencies:

npm install --save-dev typescript webpack webpack-cli ts-loader copy-webpack-plugin @types/chrome

2. Configuration Files

Create tsconfig.json for TypeScript configuration:

{
  "compilerOptions": {
    "target": "es6",
    "module": "es6",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Set up webpack.config.js:

const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: {
    background: './src/background/background.ts',
    popup: './src/popup/popup.ts',
    content: './src/content/content.ts',
  },
  // ... rest of webpack configuration
};

Core Components

1. Type Definitions

Create type definitions for Imgur API responses (src/types/imgur.ts):

export interface ImgurResponse {
  id: string;
  title: string;
  description: string;
  cover: ImgurCover;
}

export interface ImgurCover {
  id: string;
  url: string;
  width: number;
  height: number;
  type: string;
  mime_type: string;
}

2. Background Script

The background script handles API communication with Imgur:

const IMGUR_API_URL = 'https://api.imgur.com/post/v1/posts';
const CLIENT_ID = 'your_client_id';

// Handle messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  console.log('Received message:', request);

  if (request.action === 'getRandomImage') {
    console.log('Fetching random image...');
    fetchRandomImage()
      .then(response => {
        console.log('Fetch successful:', response);
        sendResponse({ success: true, data: response });
      })
      .catch(error => {
        console.error('Fetch failed:', error);
        sendResponse({ success: false, error: error.message });
      });
    return true; // Required for async response
  }
});

async function fetchRandomImage(): Promise<ImgurResponse> {
  // API communication logic
}

3. Popup Interface

Create a clean and responsive user interface (src/popup/popup.html):

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Random Imgur Image</title>
    <!-- styles here -->
  </head>
  <body>
    <button id="refreshButton">Get Random Meme</button>
    <div id="imageContainer"></div>
    <script src="popup.js"></script>
  </body>
</html>

4. Popup Logic

The popup script (src/popup/popup.ts) handles the user interface and interaction:

import { ImgurResponse } from '../types/imgur';

document.addEventListener('DOMContentLoaded', () => {
  // Get DOM elements
  const imageContainer = document.getElementById('imageContainer') as HTMLDivElement;
  const refreshButton = document.getElementById('refreshButton') as HTMLButtonElement;
  const loadingSpinner = document.getElementById('loadingSpinner') as HTMLDivElement;
  const errorMessage = document.getElementById('errorMessage') as HTMLDivElement;

  async function displayRandomImage() {
    try {
      // Show loading state
      refreshButton.disabled = true;
      loadingSpinner.style.display = 'block';
      errorMessage.style.display = 'none';

      // Request new image from background script
      const response = await chrome.runtime.sendMessage({ action: 'getRandomImage' });

      if (!response.success) {
        throw new Error(response.error);
      }

      const imgurData: ImgurResponse = response.data;

      // Create and display content
      imageContainer.innerHTML = '';

      const titleElement = document.createElement('h3');
      titleElement.textContent = imgurData.title;
      imageContainer.appendChild(titleElement);

      // Handle different media types
      if (imgurData.cover.mime_type?.startsWith('video/')) {
        const videoElement = document.createElement('video');
        videoElement.controls = false;
        videoElement.autoplay = true;
        videoElement.style.maxWidth = '100%';

        const sourceElement = document.createElement('source');
        sourceElement.src = imgurData.cover.url;
        sourceElement.type = imgurData.cover.mime_type;

        videoElement.appendChild(sourceElement);
        imageContainer.appendChild(videoElement);
      } else {
        const imgElement = document.createElement('img');
        imgElement.src = imgurData.cover.url;
        imgElement.alt = imgurData.title;
        imgElement.style.maxWidth = '100%';
        imageContainer.appendChild(imgElement);
      }

      // Reset UI state
      loadingSpinner.style.display = 'none';
      imageContainer.style.display = 'block';
      refreshButton.disabled = false;
    } catch (error) {
      // Handle errors
      console.error('Error:', error);
      loadingSpinner.style.display = 'none';
      errorMessage.style.display = 'block';
      errorMessage.textContent = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
      refreshButton.disabled = false;
    }
  }

  // Event listeners
  refreshButton.addEventListener('click', displayRandomImage);

  // Keyboard shortcuts
  document.addEventListener('keydown', (event) => {
    if (event.key === 'Enter' || event.key === ' ') {
      displayRandomImage();
    }
  });

  // Load initial image
  displayRandomImage();
});

Key Features

  • Type Safety: Full TypeScript support for Chrome APIs and custom types
  • DOM Manipulation: Safely handling DOM elements with TypeScript
  • Error Handling: Robust error handling with user-friendly messages
  • Media Support: Handles both images and videos from Imgur
  • Keyboard Controls: Shortcuts for better user experience
  • Loading States: Smooth loading transitions
  • User Experience:
    • Loading states
    • Keyboard shortcuts
    • Disabled states during loading
    • Error messages
    • Automatic initial load

The script communicates with the background script using Chrome's messaging system and handles all UI updates in a type-safe way. This makes the code more maintainable and helps catch potential errors during development.

Building and Testing

Add build scripts to package.json:

{
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "watch": "webpack --config webpack.config.js --watch"
  }
}

Build the extension:

npm run build

Load in Chrome:

  • Navigate to chrome://extensions/
  • Enable Developer Mode
  • Click "Load unpacked" and select the dist folder

Conclusion

Building Chrome extensions with TypeScript provides a robust development experience while maintaining code quality. This approach gives us type safety, better tooling, and a more maintainable codebase.

The complete code is available on GitHub. The published chrome extension is here. Feel free to explore, contribute and build upon it!


This content originally appeared on DEV Community and was authored by Cendekia


Print Share Comment Cite Upload Translate Updates
APA

Cendekia | Sciencx (2025-02-13T00:49:46+00:00) Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices. Retrieved from https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/

MLA
" » Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices." Cendekia | Sciencx - Thursday February 13, 2025, https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/
HARVARD
Cendekia | Sciencx Thursday February 13, 2025 » Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices., viewed ,<https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/>
VANCOUVER
Cendekia | Sciencx - » Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/
CHICAGO
" » Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices." Cendekia | Sciencx - Accessed . https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/
IEEE
" » Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices." Cendekia | Sciencx [Online]. Available: https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/. [Accessed: ]
rf:citation
» Crafting a Chrome Extension: TypeScript, Webpack, and Best Practices | Cendekia | Sciencx | https://www.scien.cx/2025/02/13/crafting-a-chrome-extension-typescript-webpack-and-best-practices/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.