Create a Chrome Extension with TypeScript
Learn how to create a Chrome extension with TypeScript and transpile it down to JavaScript to load as a Chrome extension.
Table of Contents 📖
- Project Creation and Setup
- Installing TypeScript and Chrome Types
- Configuring TypeScript
- Creating a manifest.json File
- Writing TypeScript Code
- Creating Scripts
- Running the Application
Project Creation and Setup
To begin, create a folder to hold the project and navigate into it. Lets call the project create-a-chrome-extension-with-typescript.
mkdir create-a-chrome-extension-with-typescript
cd create-a-chrome-extension-with-typescript
Now lets initialize the project as an ES6 npm project by using the command npm init es6 -y.
npm init es6 -y
This command turns our directory into an npm project by creating a package.json file. The -y flag adds default information about the project such as its name, version, and npm commands. Specifying ES6 allows us to ECMAScript import/export as opposed to CommonJS.
Installing TypeScript and Chrome Types
Now lets install TypeScript. TypeScript is simply an npm package called typescript. We need to install it as a development dependency with the -D flag as TypeScript is used for development but not production. To install typescript, run the command npm i typescript -D.
npm i typescript -D
To get the benefits of TypeScript, we also need to install npm packages from the @types scope. These packages contain TypeScript declaration files, or files with a .d.ts extension, that inform the TypeScript compiler about all the typing information for the specific library. For example, @types/chrome provides TypeScript type information for working with Chrome. Lets install the @types/chrome package as a development dependency from npm using the command npm i @types/chrome -D.
npm i @types/chrome -D
Configuring TypeScript
Next, we need to create a tsconfig.json file. The tsconfig.json file indicates the root of a TypeScript project and is used to configure the TypeScript compiler. We can create a tsconfig.json file using npx and tsc. Npx is an npm package runner that allows us to run executable JavaScript packages without installing them. Tsc stands for The TypeScript Compiler and it allows us to work with the TypeScript compiler. We can create a tsconfig.json file by using the command npx tsc --init.
npx tsc --init
Checking the contents of tsconfig.json, we can see the default configuration for our TypeScript compiler. Most of the contents of the file are commented out, uncommenting that line will enable that functionality. Note that most of the contents of tsconfig.json are ommitted here as there are a lot of options.
{
"compilerOptions": {
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
...
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
...
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
...
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
Some examples of configuration options are setting outDir which specifies the output directory of the transpiled JavaScript files. For a demonstration, lets set our outDir to dist.
"outDir": "dist"
Now, when we transpile our TypeScript files they will be placed in a folder called dist. Lets also create a folder inside our TypeScript app called src to hold all our source code.
mkdir src
Now lets use tsconfig.json to specify this folder as the location of our TypeScript files we want to compile. We can do this by using the top level key include.
"include": ["src/**/*"]
This tells the TypeScript compiler to compile every TypeScript file inside the src directory and its subdirectories. Now lets use tsconfig.json to specify the folder we want the TypeScript compiler to ignore. We want to ignore the node_modules folder and our output dist folder. We can ignore this folder using the top level exclude key.
"exclude": ["node_modules", "dist"]
Also, as we specified our project is using ECMAScript, we need to set the module key to NodeNext.
"module": "NodeNext"
The module key sets the module system for the transpiled JavaScript files. Setting it to NodeNext will allow us to transpile .mts files to .mjs files and .cts files to .cjs files. In other words, the transpiled files can either have ECMAScript import/export or CommonJS require/module.exports. We will be working with ECMAScript because we set the type key inside package.json to module when we ran npm init es6 -y. Finally, lets set the rootDir key to be the current directory.
"rootDir": "./"
Now when TypeScript compiles our TypeScript files, our project's structure will be maintained.
Creating a manifest.json File
Now that we have our TypeScript configuration setup, lets begin configuring our chrome extension by creating a manifest.json file.
touch manifest.json
Every chrome extension requires a manifest.json file at the root level of the directory. Now lets fill in the required information.
{
"manifest_version": 3,
"name": "Create a Chrome Extension with TypeScript",
"version": "1.0.0"
}
Now, lets add the action and background keys.
"action": {},
"background": {}
The action key allows us to use the chrome.action API to control/configure the behavior of the extension's icon in the Google Chrome toolbar. The background key allows us to specify a service worker. A chrome extension service worker is an extension's central event handler. They respond to events such as closing a tab, network calls, navigating to a new web page, etc. The background key takes a key called service_worker that accepts the path to a script.
"background": {
"service_worker": "src/background/index.js",
"type": "module"
}
Notice how we specify the service worker file as a JavaScript file and not TypeScript. This is because we will be using the compiled JavaScript code for the extension. TypeScript is for development. We also set the type key to module as we are using ECMAScript import/export inside our background scripts. Lets now create the location for the corresponding TypeScript code inside the src folder.
cd src
mkdir background
touch index.ts
Writing TypeScript Code
Now lets add some TypeScript code to our service worker. What we will do is add an onClick listener to our extension icon. When it is clicked we will inject a script that creates a button, adds an alert message to it, and then appends it to the current DOM.
/**
* When chrome extension icon is clicked, append a button to
* the DOM that when clicked sends an alert 'Hey WittCode!'
*/
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
func: () => {
const myButton: HTMLButtonElement = document.createElement('button');
myButton.textContent = 'Say hi to WittCode!';
myButton.onclick = () => {
alert('Hey WittCode!')
}
document.body.appendChild(myButton);
},
target: {
tabId: tab.id || 0
}
}).then(() => {
console.log('Button inserted');
}).catch((err) => {
console.error('Button not inserted', err);
});
})
To do this, we need to add a few permissions to manifest.json. Namely, permission for scripting and activeTab.
"permissions": ["scripting", "activeTab"],
The activeTab permission grants the extension access to the tab that the user is currently using. The scripting permission gives our extension the ability to execute scripts.
Creating Scripts
Now lets create some handy scripts for working with this project. When we build our project, we want everything that it needs to be in the dist folder that TypeScript creates. This includes our manifest.json file, node_modules, etc. First, lets create a script that will delete the dist folder and all its contents.
"clean": "rm -rf dist"
Note that these commands will be different on a Windows machine. Next, lets create a compile script to compile our TypeScript code.
"compile": "npx tsc",
This will compile our TypeScript code using the configuration inside tsconfig.json. Next, lets copy over everything else that our chrome extension needs.
"copy-assets": "rsync -av --exclude 'tsconfig.json' --exclude 'src' --exclude 'dist' ./ dist/"
The rsync command helps synchronize files and directories between two locations. Here, we are copying over everything that isn't inside our src folder, dist folder, and tsconfig.json file to our dist folder that TypeScript creates. We exclude the src folder because TypeScript has already compiled everything in there. As the project expands this command might have to change. Finally, lets run all these commands one after the other.
"build": "npm run clean && npm run compile && npm run copy-assets"
This of course could be a lot easier using the module bundler Webpack, but this tutorial focuses solely on using TypeScript.
Running the Application
Now we just need to run the command npm run build and then load the dist folder into Chrome extensions.
Here, we can see that clicking the extension icon on an active tab creates a HTML button that when clicked alerts Hey WittCode!