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!