Why You Should Care About Manifest V3 and TypeScript
If you’ve been thinking about building a Chrome extension but got intimidated by the Manifest V2 deprecation, buckle up—this is actually your moment. Manifest V3 is here to stay, and pairing it with TypeScript transforms extension development from “debugging mysterious race conditions at 2 AM” into something that actually feels professional. Let me be honest: building browser extensions used to feel like wrestling with a octopus blindfolded. But today? Today it’s more like a guided tour where TypeScript holds your hand and gently points out when you’re about to shoot yourself in the foot.
The Architecture Landscape
Before we dive into code, let’s understand how the pieces fit together. A modern Chrome extension with Manifest V3 isn’t just one thing—it’s an ecosystem of scripts, each with specific responsibilities and constraints.
React/HTML"] ContentScript["Content Script
Runs in Page Context"] ServiceWorker["Service Worker
Background Logic"] StorageAPI["Chrome Storage API"] User -->|Click Extension Icon| Popup User -->|Interact with Page| ContentScript Popup -->|Send Message| ServiceWorker ContentScript -->|Send Message| ServiceWorker ServiceWorker -->|Store Data| StorageAPI Popup -->|Read Data| StorageAPI ServiceWorker -->|Communicate| ContentScript
This architecture exists for security reasons—each component operates in different contexts with different permissions. It’s like a fortress with specialized guards, each defending a specific perimeter.
Setting Up Your Development Environment
Let’s get practical. First, create your project directory and initialize it:
mkdir my-awesome-extension
cd my-awesome-extension
npm init -y
Next, install the essential dependencies:
npm install --save-dev typescript webpack webpack-cli ts-loader @types/chrome @types/node
npm install react react-dom
npm install --save-dev @types/react @types/react-dom
Now initialize TypeScript:
npx tsc --init
This creates a tsconfig.json file. Update it to be TypeScript-strict but reasonable:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Crafting the Manifest V3 Foundation
The manifest.json file is your extension’s passport. It declares what your extension does and what it needs to do it. Here’s a well-structured example that works with TypeScript:
{
"manifest_version": 3,
"name": "My Awesome Extension",
"version": "1.0.0",
"description": "An extension that does awesome things, obviously",
"permissions": ["storage", "scripting", "tabs"],
"action": {
"default_popup": "popup.html",
"default_title": "My Awesome Extension"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"host_permissions": ["<all_urls>"]
}
Notice how we’re not using background pages anymore—just a service_worker. This is Manifest V3’s way of keeping things lean and efficient. The service worker wakes up when needed and goes back to sleep, saving system resources. Think of it as a highly trained assistant who only appears when called.
Building the Popup UI with React and TypeScript
Create src/popup/index.tsx:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import './popup.css';
interface CounterData {
clickCount: number;
lastClicked?: string;
}
const Popup: React.FC = () => {
const [count, setCount] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
chrome.storage.local.get(['clickCount'], (result) => {
setCount(result.clickCount || 0);
setLoading(false);
});
}, []);
const handleClick = async (): Promise<void> => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab.id) {
chrome.tabs.sendMessage(tab.id, { action: 'doSomethingAwesome' });
const newCount = count + 1;
setCount(newCount);
chrome.storage.local.set({
clickCount: newCount,
lastClicked: new Date().toISOString()
});
}
};
if (loading) {
return <div className="popup">Loading...</div>;
}
return (
<div className="popup">
<h1>Hello, Extension!</h1>
<p>You've clicked the button <strong>{count}</strong> times</p>
<button onClick={handleClick}>Do Something Awesome</button>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<Popup />);
And the corresponding popup.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="popup.js"></script>
</body>
</html>
The Content Script: Where the Magic Happens on the Page
Create src/content/index.ts:
interface ExtensionMessage {
action: string;
payload?: unknown;
}
interface ContentScriptResponse {
success: boolean;
message: string;
}
chrome.runtime.onMessage.addListener(
(request: ExtensionMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: ContentScriptResponse) => void) => {
if (request.action === 'doSomethingAwesome') {
try {
// Your awesome logic here
const elements = document.querySelectorAll('body');
elements.forEach((element) => {
element.style.borderRadius = '8px';
element.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
});
sendResponse({
success: true,
message: 'Something awesome was done!'
});
} catch (error) {
sendResponse({
success: false,
message: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
});
}
}
}
);
The Service Worker: Your Extension’s Brain
Create src/background/index.ts:
interface StorageData {
clickCount: number;
lastClicked?: string;
extensionEnabled: boolean;
}
// Initialize storage when extension is installed
chrome.runtime.onInstalled.addListener((): void => {
chrome.storage.local.set({
clickCount: 0,
extensionEnabled: true
} as StorageData);
console.log('Extension installed and ready to rock!');
});
// Listen for messages from content scripts and popup
chrome.runtime.onMessage.addListener(
(request: { action: string }, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) => {
if (request.action === 'getStats') {
chrome.storage.local.get(['clickCount', 'lastClicked'], (data) => {
sendResponse({
stats: data
});
});
return true; // Keep the message channel open for sendResponse
}
}
);
// Perform periodic tasks
chrome.alarms.create('dailyCleanup', { periodInMinutes: 60 });
chrome.alarms.onAlarm.addListener((alarm: chrome.alarms.Alarm): void => {
if (alarm.name === 'dailyCleanup') {
console.log('Performing cleanup tasks...');
}
});
Webpack Configuration: Tying It All Together
Create webpack.config.js:
const path = require('path');
module.exports = [
{
mode: 'production',
entry: {
popup: './src/popup/index.tsx',
content: './src/content/index.ts',
background: './src/background/index.ts'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
}
}
];
Add this build script to your package.json:
{
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
}
}
Building and Testing Your Extension
Run the build:
npm run build
Now, here’s where it gets fun. Open Chrome and navigate to chrome://extensions/. In the top-right corner, flip the Developer mode toggle. Click Load unpacked and select your dist folder.
Your extension should appear in the extensions list. Click its icon to test the popup. Open the DevTools console for the service worker (there’s a link on the extensions page) to see your logs. This is where you’ll spend quality time debugging why your code isn’t doing what you think it’s doing.
TypeScript Tips for Extension Development
Strongly type your messages. This prevents the dreaded “undefined is not a function” errors at 3 AM:
type ExtensionMessages =
| { action: 'doSomethingAwesome' }
|--|--|
| { action: 'enableFeature'; feature: string };
Use generics for storage interactions. Type-safe storage prevents mysterious bugs:
const getStorageData = <T>(key: string): Promise<T> => {
return new Promise((resolve) => {
chrome.storage.local.get([key], (result) => {
resolve(result[key] as T);
});
});
};
Handle permissions carefully. Always check before using restricted APIs:
const hasPermission = (permission: string): Promise<boolean> => {
return chrome.permissions.contains({ permissions: [permission] });
};
Deployment and Distribution
When you’re ready to share your creation with the world, package your dist folder as a ZIP file. Then head to the Chrome Web Store Developer Dashboard. You’ll need a developer account (one-time $5 fee), but after that, you’re free to distribute your extension to millions of potential users.
The review process typically takes a few hours to a couple of days. Google will check that your extension actually does what it claims and doesn’t do anything nefarious.
Common Pitfalls and How to Avoid Them
The storage racing condition: Don’t assume chrome.storage.get returns immediately. Always use callbacks or promises:
// ❌ This doesn't work
const data = chrome.storage.local.get(['key']);
console.log(data); // undefined!
// ✅ This works
chrome.storage.local.get(['key'], (result) => {
console.log(result.key); // Now it's defined
});
Service worker timeout: Your service worker might be unloaded while performing long operations. Use background pages cautiously or break long tasks into smaller chunks. Content script isolation: Content scripts run in the page context but are isolated. You can’t directly access page variables. If you need to, inject a script into the page itself.
Moving Forward
Manifest V3 represents a maturation of the extension platform. Yes, some things are stricter. Yes, some APIs changed. But the result is a more secure, predictable ecosystem. Pair it with TypeScript, and you have a genuinely pleasant development experience. The beautiful thing about building extensions today is that you’re not working in a vacuum—there’s an entire community of developers facing the same challenges, and solutions are a quick search away. Welcome to the extension developer club. We have stickers.
