Chrome Extension TypeScript Setup — Type-Safe Extension Development
33 min readTypeScript Configuration for Chrome Extensions
TypeScript brings type safety, better tooling, and maintainability to Chrome extension development. This guide covers the definitive setup for TypeScript in Manifest V3 extensions, from compiler configuration to advanced patterns for typed messaging and storage.
Table of Contents
- Base tsconfig.json for Chrome Extensions
- Chrome API Type Definitions
- Multiple tsconfig Files for Different Contexts
- Project References for Shared Types
- Shared Message Types and Storage Schemas
- Path Aliases for Clean Imports
- Declaration Files for Chrome APIs
- Strict Mode Recommendations
- Build Configuration with Vite and TypeScript
- Common TypeScript Errors and Fixes
- Type Guards for Message Handling
- Generic Patterns for Typed Storage and Messaging
Base tsconfig.json for Chrome Extensions
Chrome extensions run in a browser environment, but the service worker context lacks DOM APIs. A well-tuned tsconfig.json accounts for these constraints.
// tsconfig.json (base configuration)
{
"compilerOptions": {
// Target ES2022 for top-level await, private fields, and Array.at()
"target": "ES2022",
// ESNext modules; bundler will handle resolution
"module": "ESNext",
"moduleResolution": "bundler",
// Include DOM for popup/content scripts, Chrome types separately
"lib": ["ES2022", "DOM", "DOM.Iterable"],
// Output settings
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Strict type checking (see Strict Mode section)
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Module interop
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
// Skip type checking node_modules
"skipLibCheck": true,
// Resolve JSON imports (useful for manifest)
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Key Setting Rationale
| Setting | Value | Why |
|---|---|---|
target |
ES2022 |
Chrome 109+ supports ES2022 natively; no transpilation overhead |
module |
ESNext |
Lets Vite/webpack handle module bundling |
moduleResolution |
bundler |
Aligns with how modern bundlers resolve imports |
lib |
ES2022, DOM |
ES2022 for language features, DOM for content scripts and popups |
isolatedModules |
true |
Required by most bundlers (Vite, esbuild) for per-file transpilation |
noUncheckedIndexedAccess |
true |
Prevents unsafe property access on objects and arrays |
Chrome API Type Definitions
Several npm packages provide TypeScript definitions for the chrome.* namespace. Understanding the differences matters.
Package Comparison
| Package | Source | Notes |
|---|---|---|
chrome-types |
Auto-generated from Chromium source | Most accurate, updated frequently |
@anthropic-ai/chrome-types |
Deprecated / unavailable | Do not use |
@types/chrome |
DefinitelyTyped community | Widely used, may lag behind Chrome releases |
Installation and Setup
# Option 1: chrome-types (auto-generated from Chromium, most accurate)
npm install -D chrome-types
# Option 2: @types/chrome (DefinitelyTyped, widely used)
npm install -D @types/chrome
After installation, add the types to your tsconfig.json:
{
"compilerOptions": {
"types": ["chrome-types"]
// or for @types/chrome: "types": ["chrome"]
}
}
Alternatively, use a triple-slash directive at the top of files that use Chrome APIs:
/// <reference types="chrome-types" />
// or for @types/chrome:
/// <reference types="chrome" />
Verifying Types Work
// This should compile without errors
chrome.runtime.onInstalled.addListener((details) => {
// 'details' is typed as chrome.runtime.InstalledDetails
console.log(`Installed: ${details.reason}`);
});
// This should produce a type error
chrome.runtime.onInstalled.addListener((details) => {
details.nonExistentProperty; // Error: Property does not exist
});
Multiple tsconfig Files for Different Contexts
Chrome extensions have three distinct execution contexts, each with different available APIs. Use separate tsconfig files to enforce correct API usage per context.
Directory Structure
my-extension/
src/
background/
service-worker.ts
alarms.ts
content/
injector.ts
observer.ts
popup/
popup.ts
components/
shared/
types.ts
messages.ts
storage.ts
tsconfig.json # Base config
tsconfig.background.json
tsconfig.content.json
tsconfig.popup.json
Background Script Config
The service worker has no DOM access. Exclude DOM from lib:
// tsconfig.background.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "WebWorker"],
"outDir": "dist/background",
"rootDir": "src",
"types": ["chrome-types"]
},
"include": [
"src/background/**/*.ts",
"src/shared/**/*.ts"
]
}
Using "lib": ["ES2022", "WebWorker"] instead of "DOM" prevents accidentally referencing document, window, or other DOM globals in the service worker – the compiler will flag them as errors.
Content Script Config
Content scripts have DOM access but limited Chrome API access:
// tsconfig.content.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"outDir": "dist/content",
"rootDir": "src",
"types": ["chrome-types"]
},
"include": [
"src/content/**/*.ts",
"src/shared/**/*.ts"
]
}
Popup/Options Page Config
Popup pages are standard web pages with full DOM and Chrome API access:
// tsconfig.popup.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"outDir": "dist/popup",
"rootDir": "src",
"jsx": "react-jsx",
"types": ["chrome-types"]
},
"include": [
"src/popup/**/*.ts",
"src/popup/**/*.tsx",
"src/shared/**/*.ts"
]
}
Running Type Checks Per Context
# Check each context independently
npx tsc --project tsconfig.background.json --noEmit
npx tsc --project tsconfig.content.json --noEmit
npx tsc --project tsconfig.popup.json --noEmit
Add these to package.json scripts:
{
"scripts": {
"typecheck": "npm run typecheck:bg && npm run typecheck:content && npm run typecheck:popup",
"typecheck:bg": "tsc -p tsconfig.background.json --noEmit",
"typecheck:content": "tsc -p tsconfig.content.json --noEmit",
"typecheck:popup": "tsc -p tsconfig.popup.json --noEmit"
}
}
Project References for Shared Types
TypeScript project references let you split a project into smaller pieces with explicit dependency relationships. This is ideal for extension contexts that share types.
// tsconfig.json (root, references only)
{
"files": [],
"references": [
{ "path": "./tsconfig.shared.json" },
{ "path": "./tsconfig.background.json" },
{ "path": "./tsconfig.content.json" },
{ "path": "./tsconfig.popup.json" }
]
}
// tsconfig.shared.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist/shared",
"rootDir": "src/shared",
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true
},
"include": ["src/shared/**/*.ts"]
}
Each context config then references the shared project:
// tsconfig.background.json (with references)
{
"compilerOptions": {
"composite": true,
"lib": ["ES2022", "WebWorker"],
"outDir": "dist/background",
"rootDir": "src",
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["chrome-types"]
},
"include": ["src/background/**/*.ts"],
"references": [
{ "path": "./tsconfig.shared.json" }
]
}
Build with --build to respect references:
npx tsc --build
Shared Message Types and Storage Schemas
Define a single source of truth for message payloads and storage shapes used across all contexts.
Message Type Definitions
// src/shared/messages.ts
/** All possible message types in the extension */
export type MessageType =
| 'FETCH_DATA'
| 'UPDATE_SETTINGS'
| 'CONTENT_EXTRACTED'
| 'TAB_ACTIVATED'
| 'BADGE_UPDATE';
/** Base message shape */
interface BaseMessage<T extends MessageType, P = void> {
type: T;
payload: P;
timestamp: number;
}
/** Message payloads by type */
export type FetchDataMessage = BaseMessage<'FETCH_DATA', {
url: string;
options?: RequestInit;
}>;
export type UpdateSettingsMessage = BaseMessage<'UPDATE_SETTINGS', {
theme: 'light' | 'dark';
notifications: boolean;
}>;
export type ContentExtractedMessage = BaseMessage<'CONTENT_EXTRACTED', {
title: string;
text: string;
url: string;
}>;
export type TabActivatedMessage = BaseMessage<'TAB_ACTIVATED', {
tabId: number;
windowId: number;
}>;
export type BadgeUpdateMessage = BaseMessage<'BADGE_UPDATE', {
count: number;
color?: string;
}>;
/** Union of all messages */
export type ExtensionMessage =
| FetchDataMessage
| UpdateSettingsMessage
| ContentExtractedMessage
| TabActivatedMessage
| BadgeUpdateMessage;
/** Response types mapped to message types */
export type MessageResponseMap = {
FETCH_DATA: { data: unknown; status: number };
UPDATE_SETTINGS: { success: boolean };
CONTENT_EXTRACTED: { saved: boolean; id: string };
TAB_ACTIVATED: void;
BADGE_UPDATE: void;
};
Storage Schema
// src/shared/storage.ts
/** Shape of data in chrome.storage.local */
export interface LocalStorageSchema {
settings: {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
};
cache: {
lastFetch: number;
data: Record<string, unknown>;
};
history: Array<{
url: string;
visitedAt: number;
title: string;
}>;
}
/** Shape of data in chrome.storage.sync */
export interface SyncStorageSchema {
preferences: {
fontSize: number;
showBadge: boolean;
};
savedItems: string[];
}
Path Aliases for Clean Imports
Path aliases prevent deeply nested relative imports like ../../../shared/types.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["src/shared/*"],
"@background/*": ["src/background/*"],
"@content/*": ["src/content/*"],
"@popup/*": ["src/popup/*"]
}
}
}
Usage in source files:
// Before: messy relative imports
import { ExtensionMessage } from '../../../shared/messages';
import { LocalStorageSchema } from '../../../shared/storage';
// After: clean alias imports
import { ExtensionMessage } from '@shared/messages';
import { LocalStorageSchema } from '@shared/storage';
Vite Alias Configuration
Vite needs its own alias configuration to resolve these paths at build time:
// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
resolve: {
alias: {
'@shared': resolve(__dirname, 'src/shared'),
'@background': resolve(__dirname, 'src/background'),
'@content': resolve(__dirname, 'src/content'),
'@popup': resolve(__dirname, 'src/popup'),
},
},
});
Declaration Files for Chrome APIs
When Chrome types packages lag behind the latest APIs or you use experimental features, write ambient declaration files to fill the gaps.
// src/types/chrome-extensions.d.ts
/**
* Ambient declarations for Chrome APIs not yet in published types.
* Remove entries as official types catch up.
*/
declare namespace chrome.sidePanel {
interface SidePanelOptions {
tabId?: number;
path?: string;
enabled?: boolean;
}
function setOptions(options: SidePanelOptions): Promise<void>;
function getOptions(options: { tabId?: number }): Promise<SidePanelOptions>;
function open(options: { tabId?: number; windowId?: number }): Promise<void>;
function setPanelBehavior(
behavior: { openPanelOnActionClick: boolean }
): Promise<void>;
}
declare namespace chrome.readingList {
interface ReadingListEntry {
url: string;
title: string;
hasBeenRead: boolean;
lastUpdateTime: number;
creationTime: number;
}
function addEntry(entry: {
url: string;
title: string;
hasBeenRead?: boolean;
}): Promise<void>;
function removeEntry(info: { url: string }): Promise<void>;
function query(info: Partial<ReadingListEntry>): Promise<ReadingListEntry[]>;
function updateEntry(info: {
url: string;
title?: string;
hasBeenRead?: boolean;
}): Promise<void>;
}
Include the declaration file in your tsconfig:
{
"include": [
"src/**/*.ts",
"src/types/**/*.d.ts"
]
}
Strict Mode Recommendations
Enable all strict checks. Each one prevents a real category of bugs in extension code.
{
"compilerOptions": {
// The "strict" flag enables all of these:
"strict": true,
// Plus these additional strict checks:
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
}
}
Why Each Flag Matters for Extensions
strictNullChecks (included in strict): Chrome APIs frequently return undefined. Tabs may not exist, storage may be empty.
// Without strictNullChecks -- silent bug
const tab = await chrome.tabs.get(tabId);
console.log(tab.url.length); // Runtime crash if tab.url is undefined
// With strictNullChecks -- caught at compile time
const tab = await chrome.tabs.get(tabId);
console.log(tab.url?.length); // Must handle undefined
noUncheckedIndexedAccess: Storage data and message payloads often use dynamic keys.
const data = await chrome.storage.local.get('settings');
// Without: data['settings'] is typed as any
// With: data['settings'] is typed as unknown | undefined -- must check
exactOptionalPropertyTypes: Prevents passing undefined where a property should be omitted entirely. Relevant for Chrome API options objects.
// With exactOptionalPropertyTypes
interface NotificationOptions {
title: string;
iconUrl?: string; // means "string if present", NOT "string | undefined"
}
// Error: undefined is not assignable
const opts: NotificationOptions = { title: 'Hi', iconUrl: undefined };
// Correct: omit the property
const opts: NotificationOptions = { title: 'Hi' };
Build Configuration with Vite and TypeScript
Vite provides fast builds with native ESM support. Here is a complete Vite setup for a Chrome extension.
// vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
// Output to dist/ for loading as unpacked extension
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
input: {
// Each entry point becomes a separate bundle
background: resolve(__dirname, 'src/background/service-worker.ts'),
content: resolve(__dirname, 'src/content/injector.ts'),
popup: resolve(__dirname, 'src/popup/popup.html'),
options: resolve(__dirname, 'src/options/options.html'),
},
output: {
// Predictable file names (no hashes) for manifest.json references
entryFileNames: '[name]/index.js',
chunkFileNames: 'shared/[name].js',
assetFileNames: 'assets/[name].[ext]',
},
},
// No minification during development for easier debugging
minify: process.env.NODE_ENV === 'production',
sourcemap: process.env.NODE_ENV !== 'production' ? 'inline' : false,
// Target Chrome's V8 directly
target: 'esnext',
},
resolve: {
alias: {
'@shared': resolve(__dirname, 'src/shared'),
'@background': resolve(__dirname, 'src/background'),
'@content': resolve(__dirname, 'src/content'),
'@popup': resolve(__dirname, 'src/popup'),
},
},
});
Package Scripts
{
"scripts": {
"dev": "vite build --watch --mode development",
"build": "tsc --noEmit && vite build --mode production",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
}
}
Using CRXJS Vite Plugin
The @crxjs/vite-plugin simplifies extension builds by reading manifest.json directly:
// vite.config.ts with CRXJS
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
import manifest from './manifest.json';
export default defineConfig({
plugins: [crx({ manifest })],
build: {
target: 'esnext',
},
});
This approach lets Vite auto-discover entry points from the manifest and handles HMR for popup and options pages during development.
Common TypeScript Errors and Fixes
Error: Property does not exist on type ‘chrome’
Cause: Missing or outdated type definitions.
# Fix: install or update chrome types
npm install -D chrome-types@latest
Error: Cannot find name ‘document’ in service worker
Cause: Using DOM APIs in a service worker context.
// Wrong: DOM not available in service worker
document.getElementById('app'); // Error with WebWorker lib
// Right: use message passing to have content script interact with DOM
chrome.tabs.sendMessage(tabId, { type: 'GET_ELEMENT' });
Error: Type ‘void’ is not assignable to type ‘boolean | undefined’
Cause: onMessage listener return type mismatch. The listener must return true to indicate it will send an asynchronous response.
// Wrong
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetchData().then((data) => sendResponse(data));
// Implicitly returns void, but Chrome expects true for async
});
// Right
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetchData().then((data) => sendResponse(data));
return true; // Keep the message channel open
});
Error: Argument of type ‘string’ is not assignable to parameter
Cause: Chrome APIs expect string literal unions, not plain string.
// Wrong
const reason: string = 'install';
if (details.reason === reason) { } // Works but loses type narrowing
// Right
const reason: chrome.runtime.OnInstalledReason = 'install';
if (details.reason === reason) { } // Proper literal type
Error: Promise returned but not awaited
Cause: MV3 Chrome APIs return promises, but older patterns used callbacks.
// Old callback style (still works but types may conflict)
chrome.storage.local.get(['key'], (result) => { });
// Modern async/await style
const result = await chrome.storage.local.get(['key']);
Type Guards for Message Handling
Type guards narrow the ExtensionMessage union to specific message types, giving you safe access to payload properties.
Basic Type Guard
import type { ExtensionMessage, MessageType } from '@shared/messages';
function isMessageType<T extends MessageType>(
message: ExtensionMessage,
type: T
): message is Extract<ExtensionMessage, { type: T }> {
return message.type === type;
}
// Usage in message listener
chrome.runtime.onMessage.addListener((message: ExtensionMessage, sender, sendResponse) => {
if (isMessageType(message, 'FETCH_DATA')) {
// message.payload is typed as { url: string; options?: RequestInit }
fetch(message.payload.url, message.payload.options)
.then((res) => res.json())
.then((data) => sendResponse({ data, status: 200 }));
return true;
}
if (isMessageType(message, 'UPDATE_SETTINGS')) {
// message.payload is typed as { theme: 'light' | 'dark'; notifications: boolean }
applySettings(message.payload);
sendResponse({ success: true });
}
});
Exhaustive Message Handler
Use a switch statement with never checking to ensure all message types are handled:
function handleMessage(message: ExtensionMessage): void {
switch (message.type) {
case 'FETCH_DATA':
handleFetchData(message.payload);
break;
case 'UPDATE_SETTINGS':
handleUpdateSettings(message.payload);
break;
case 'CONTENT_EXTRACTED':
handleContentExtracted(message.payload);
break;
case 'TAB_ACTIVATED':
handleTabActivated(message.payload);
break;
case 'BADGE_UPDATE':
handleBadgeUpdate(message.payload);
break;
default:
// If you add a new MessageType but forget to handle it here,
// TypeScript will report an error on this line
const _exhaustive: never = message;
throw new Error(`Unhandled message type: ${(_exhaustive as ExtensionMessage).type}`);
}
}
Runtime Validation
For messages received from external sources (other extensions or web pages), validate at runtime:
function isValidExtensionMessage(value: unknown): value is ExtensionMessage {
if (typeof value !== 'object' || value === null) return false;
const obj = value as Record<string, unknown>;
if (typeof obj.type !== 'string') return false;
if (typeof obj.timestamp !== 'number') return false;
const validTypes: MessageType[] = [
'FETCH_DATA', 'UPDATE_SETTINGS', 'CONTENT_EXTRACTED',
'TAB_ACTIVATED', 'BADGE_UPDATE',
];
return validTypes.includes(obj.type as MessageType);
}
Generic Patterns for Typed Storage and Messaging
Typed Storage Wrapper
Wrap chrome.storage with generics so that keys and values are always type-safe:
// src/shared/typed-storage.ts
import type { LocalStorageSchema, SyncStorageSchema } from './storage';
type StorageArea = 'local' | 'sync';
type SchemaFor<A extends StorageArea> = A extends 'local'
? LocalStorageSchema
: SyncStorageSchema;
class TypedStorage<A extends StorageArea> {
private area: chrome.storage.StorageArea;
constructor(area: A) {
this.area = area === 'local' ? chrome.storage.local : chrome.storage.sync;
}
async get<K extends keyof SchemaFor<A>>(
key: K
): Promise<SchemaFor<A>[K] | undefined> {
const result = await this.area.get(key as string);
return result[key as string] as SchemaFor<A>[K] | undefined;
}
async set<K extends keyof SchemaFor<A>>(
key: K,
value: SchemaFor<A>[K]
): Promise<void> {
await this.area.set({ [key as string]: value });
}
async remove<K extends keyof SchemaFor<A>>(key: K): Promise<void> {
await this.area.remove(key as string);
}
onChange<K extends keyof SchemaFor<A>>(
key: K,
callback: (newValue: SchemaFor<A>[K], oldValue: SchemaFor<A>[K]) => void
): void {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== (this.area === chrome.storage.local ? 'local' : 'sync')) {
return;
}
const change = changes[key as string];
if (change) {
callback(
change.newValue as SchemaFor<A>[K],
change.oldValue as SchemaFor<A>[K]
);
}
});
}
}
// Export typed instances
export const localStorage = new TypedStorage('local');
export const syncStorage = new TypedStorage('sync');
Usage:
import { localStorage } from '@shared/typed-storage';
// Fully typed -- key must be keyof LocalStorageSchema, value matches
await localStorage.set('settings', {
theme: 'dark',
notifications: true,
language: 'en',
});
// Type error: 'invalid' is not a valid key
await localStorage.get('invalid');
// Type error: wrong value shape
await localStorage.set('settings', { wrong: true });
Typed Message Sender
// src/shared/typed-messaging.ts
import type {
ExtensionMessage,
MessageType,
MessageResponseMap,
} from './messages';
/** Send a typed message and get a typed response */
export async function sendMessage<T extends MessageType>(
message: Extract<ExtensionMessage, { type: T }>
): Promise<MessageResponseMap[T]> {
return chrome.runtime.sendMessage(message);
}
/** Send a typed message to a specific tab */
export async function sendTabMessage<T extends MessageType>(
tabId: number,
message: Extract<ExtensionMessage, { type: T }>
): Promise<MessageResponseMap[T]> {
return chrome.tabs.sendMessage(tabId, message);
}
/** Helper to create a properly typed message */
export function createMessage<T extends MessageType>(
type: T,
payload: Extract<ExtensionMessage, { type: T }>['payload']
): Extract<ExtensionMessage, { type: T }> {
return {
type,
payload,
timestamp: Date.now(),
} as Extract<ExtensionMessage, { type: T }>;
}
Usage:
import { sendMessage, createMessage } from '@shared/typed-messaging';
// Type-safe message creation and sending
const message = createMessage('FETCH_DATA', {
url: 'https://api.example.com/data',
});
const response = await sendMessage(message);
// response is typed as { data: unknown; status: number }
// Type error: wrong payload for this message type
const bad = createMessage('FETCH_DATA', { theme: 'dark' });
Typed Event Emitter for Internal Communication
// src/shared/typed-events.ts
type EventMap = {
settingsChanged: { key: string; value: unknown };
dataFetched: { url: string; data: unknown };
error: { code: string; message: string };
};
class TypedEventEmitter {
private listeners = new Map<string, Set<Function>>();
on<K extends keyof EventMap>(
event: K,
handler: (data: EventMap[K]) => void
): () => void {
const handlers = this.listeners.get(event as string) ?? new Set();
handlers.add(handler);
this.listeners.set(event as string, handlers);
// Return unsubscribe function
return () => handlers.delete(handler);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
const handlers = this.listeners.get(event as string);
if (handlers) {
handlers.forEach((handler) => handler(data));
}
}
}
export const events = new TypedEventEmitter();
Summary
A well-configured TypeScript setup for Chrome extensions involves:
- Separate tsconfig files per execution context to prevent DOM usage in service workers and enforce API boundaries.
- Project references for shared types that are consumed by all contexts.
- Strict compiler flags to catch the null-safety and type-narrowing bugs that Chrome APIs are prone to.
- Typed wrappers around
chrome.storageandchrome.runtime.sendMessageto eliminateanyat API boundaries. - Type guards and exhaustive switches for message handling to ensure every message type is accounted for.
- Path aliases synced between
tsconfig.jsonand your bundler for clean imports across contexts.
These patterns scale from small utility extensions to complex multi-context applications with dozens of message types and storage schemas.
Related Articles
Related Articles
- Dev Tools
- Webpack Setup -e —
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.