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

Cendekia - Feb 13 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

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

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"]
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Build the extension:

npm run build
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . .