Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/crxjs/chrome-extension-tools/llms.txt

Use this file to discover all available pages before exploring further.

CRXJS fully supports TypeScript out of the box. This guide covers TypeScript configuration, Chrome API types, and best practices for type-safe extension development.

Basic TypeScript Setup

Install TypeScript and Chrome types:
npm install -D typescript @types/chrome @types/node

TypeScript Configuration

Create a tsconfig.json for your project:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "moduleDetection": "force",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["vite/client", "chrome"],
    "allowImportingTsExtensions": true,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Project References

For larger projects, use TypeScript project references:
tsconfig.json
{
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "files": []
}
tsconfig.app.json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "types": ["vite/client", "chrome"],
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
tsconfig.node.json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["node"],
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["vite.config.ts", "manifest.config.ts"]
}

Chrome API Types

The @types/chrome package provides complete types for Chrome extension APIs.

Using Chrome APIs

background.ts
// Chrome APIs are fully typed
chrome.runtime.onInstalled.addListener((details) => {
  console.log('Extension installed:', details.reason)
})

// TypeScript knows the shape of chrome.tabs.Tab
chrome.tabs.query({ active: true }, (tabs: chrome.tabs.Tab[]) => {
  const activeTab = tabs[0]
  console.log('Active tab:', activeTab.title)
})

Promises with Chrome APIs

Chrome MV3 APIs support both callbacks and promises:
// Using promises (recommended)
const tabs = await chrome.tabs.query({ active: true })
console.log('Active tabs:', tabs)

// Using callbacks
chrome.tabs.query({ active: true }, (tabs) => {
  console.log('Active tabs:', tabs)
})

Manifest Types

Use defineManifest for type-safe manifest configuration:
manifest.config.ts
import { defineManifest } from '@crxjs/vite-plugin'
import pkg from './package.json'

export default defineManifest({
  manifest_version: 3,
  name: pkg.name,
  version: pkg.version,
  icons: {
    48: 'public/logo.png',
  },
  action: {
    default_icon: {
      48: 'public/logo.png',
    },
    default_popup: 'src/popup/index.html',
  },
  background: {
    service_worker: 'src/background.ts',
    type: 'module',
  },
  permissions: [
    'storage',
    'tabs',
  ],
  content_scripts: [{
    js: ['src/content/main.ts'],
    matches: ['https://*/*'],
  }],
})
defineManifest provides full TypeScript IntelliSense and type checking for your manifest.

Path Aliases

Configure path aliases for cleaner imports:
tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}
vite.config.ts
import path from 'node:path'
import { defineConfig } from 'vite'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
  },
})
Now you can import with clean paths:
import { Button } from '@components/Button'
import { formatDate } from '@utils/date'

Type-safe Message Passing

Create type-safe communication between extension contexts:
types/messages.ts
export type Message =
  | { type: 'GET_TAB_INFO'; tabId: number }
  | { type: 'UPDATE_BADGE'; text: string }
  | { type: 'SAVE_DATA'; data: Record<string, unknown> }

export type MessageResponse<T extends Message> = 
  T extends { type: 'GET_TAB_INFO' } ? chrome.tabs.Tab :
  T extends { type: 'UPDATE_BADGE' } ? { success: boolean } :
  T extends { type: 'SAVE_DATA' } ? { success: boolean } :
  never
utils/messaging.ts
import type { Message, MessageResponse } from '@/types/messages'

export async function sendMessage<T extends Message>(
  message: T
): Promise<MessageResponse<T>> {
  return chrome.runtime.sendMessage(message)
}
Use it in your extension:
import { sendMessage } from '@/utils/messaging'

// TypeScript knows the response type!
const tab = await sendMessage({ 
  type: 'GET_TAB_INFO', 
  tabId: 123 
})

const result = await sendMessage({ 
  type: 'UPDATE_BADGE', 
  text: 'New' 
})

Type-safe Storage

Create typed wrappers for Chrome storage:
utils/storage.ts
interface StorageSchema {
  theme: 'light' | 'dark'
  count: number
  user: {
    name: string
    email: string
  }
}

export async function getStorage<K extends keyof StorageSchema>(
  key: K
): Promise<StorageSchema[K] | undefined> {
  const result = await chrome.storage.sync.get(key)
  return result[key]
}

export async function setStorage<K extends keyof StorageSchema>(
  key: K,
  value: StorageSchema[K]
): Promise<void> {
  await chrome.storage.sync.set({ [key]: value })
}
Use it with full type safety:
// TypeScript knows theme is 'light' | 'dark'
const theme = await getStorage('theme')

// TypeScript enforces the correct type
await setStorage('theme', 'dark') // ✓
await setStorage('theme', 'blue') // ✗ Type error!

// Complex objects are also typed
const user = await getStorage('user')
console.log(user?.name, user?.email)

Import Assertions

Import JSON files with type safety:
import pkg from './package.json' with { type: 'json' }

console.log(pkg.name, pkg.version)

Framework-Specific TypeScript

tsconfig.app.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "types": ["vite/client", "chrome"]
  }
}

Build Script

Add TypeScript type checking to your build:
package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "type-check": "tsc -b --noEmit"
  }
}

Best Practices

Use Strict Mode

Enable strict TypeScript checks:
{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  }
}

Avoid any

Use unknown instead of any when type is uncertain:
// Bad
function handleMessage(message: any) {
  console.log(message.type)
}

// Good
function handleMessage(message: unknown) {
  if (typeof message === 'object' && message !== null) {
    console.log((message as Message).type)
  }
}

Use Type Guards

Create type guards for runtime type checking:
function isTab(obj: unknown): obj is chrome.tabs.Tab {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'url' in obj
  )
}

chrome.runtime.onMessage.addListener((message) => {
  if (isTab(message)) {
    console.log('Received tab:', message.url)
  }
})

Next Steps

Build docs developers (and LLMs) love