Chrome Extension TypeScript Extensions — Developer Guide

4 min read

TypeScript for Chrome Extensions

TypeScript brings type safety, autocompletion, and confident refactoring to Chrome extension development.

Overview

Why TypeScript for Extensions

Chrome API Types

Install @types/chrome for all Chrome extension API types:

npm install --save-dev @types/chrome

Project Setup

mkdir my-extension && cd my-extension
npm init -y
npm install --save-dev typescript @types/chrome esbuild
mkdir src

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "dist",
    "rootDir": "src",
    "types": ["chrome"]
  },
  "include": ["src"]
}

Build Pipeline

import * as esbuild from 'esbuild';
import { copyFileSync, mkdirSync, existsSync } from 'fs';

const entries = [
  { in: 'src/background.ts', out: 'background.js' },
  { in: 'src/popup.ts', out: 'popup.js' },
  { in: 'src/content.ts', out: 'content.js' },
];

const config: esbuild.BuildOptions = {
  bundle: true, sourcemap: true, target: 'chrome110',
  outdir: 'dist', format: 'iife',
};

async function build() {
  if (!existsSync('dist')) mkdirSync('dist');
  copyFileSync('manifest.json', 'dist/manifest.json');
  await Promise.all(entries.map(e =>
    esbuild.build({ ...config, entryPoints: [e.in], outfile: `dist/${e.out}` })
  ));
}
build();

Typing Chrome APIs

Automatic Types

const tabs = await chrome.tabs.query({ active: true });
// tabs is typed as chrome.tabs.Tab[]

Typed Message Passing

Define messages as discriminated unions:

type MsgFromContent = { type: 'FETCH_URL'; url: string };
type MsgFromBg = { type: 'URL_DATA'; title: string };
type AllMessages = MsgFromContent | MsgFromBg;

const resp = await chrome.runtime.sendMessage<AllMessages, MsgFromBg>({
  type: 'FETCH_URL', url: 'https://example.com'
});

chrome.runtime.onMessage.addListener((msg: AllMessages, s, reply) => {
  if (msg.type === 'FETCH_URL') reply({ type: 'URL_DATA', title: 'Ex' });
  return true;
});

Use @theluckystrike/webext-messaging for simpler typed wrappers.

Typed Storage

interface ExtensionSettings {
  theme: 'light' | 'dark';
  notifications: boolean;
}

async function getSettings(): Promise<ExtensionSettings> {
  const result = await chrome.storage.local.get('settings') as { settings?: ExtensionSettings };
  return result.settings ?? { theme: 'light', notifications: true };
}

@theluckystrike/webext-storage provides typed get/set helpers.

Context-Specific Types

Content Scripts (DOM)

{ "compilerOptions": { "lib": ["ES2020", "DOM"] } }

Access chrome.runtime, chrome.storage—not chrome.tabs. Use messaging.

Service Workers (WebWorker)

{ "compilerOptions": { "lib": ["ES2020", "WebWorker"] } }

Summary

  1. Install @types/chrome for automatic Chrome API typing
  2. Use esbuild for fast builds; Vite for framework UIs
  3. Define message types as discriminated unions
  4. Create storage interfaces instead of using any
  5. Use separate tsconfigs: DOM for content scripts, WebWorker for service workers

Cross-references:

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.