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. 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 { // API communication logic } 3. Popup Interface Create a clean and responsive user interface (src/popup/popup.html): Random Imgur Image Get Random Meme 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;

Feb 13, 2025 - 03:56
 0
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. 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):



  
     charset="UTF-8">
    </span>Random Imgur Image<span class="nt">
    
  
  
     id="refreshButton">Get Random Meme
     id="imageContainer">

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!