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:
{
"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:
{
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"files": []
}
{
"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"]
}
{
"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
// 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:
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:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
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:
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
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:
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
{
"compilerOptions": {
"jsx": "react-jsx",
"types": ["vite/client", "chrome"]
}
}
{
"compilerOptions": {
"jsx": "preserve",
"types": ["vite/client", "chrome"]
}
}
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"types": ["vite/client", "chrome"]
}
}
Build Script
Add TypeScript type checking to your build:
{
"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