I’m writing this post because I had trouble finding a straightforward answer to this question: what are the current ways to use TypeScript with Service Worker files? And, to take it a step further, how to use those types in JavaScript, with JSDoc comments?
The examples in this post are specifically for a Service Worker script, but in general the solutions and issues in this post apply to web workers in general as well.
The Basics of Where Things are At
First, before getting into the solutions, I want to bring up a few things that I found in my research that are important to note. Keep in mind that these things might change, so this is kind of a “here things are now” section:
- You no longer need external type definitions for web workers / service workers
- A lot of older posts and StackOverflow answers will suggest things like copying and pasting code out of a Gist, using
@types
(DefinitelyTyped), or some other external type defs. These used to be necessary, but Web Worker types are now shipped by default with TypeScript, in the lib filelib.webworker.d.ts
. How to use this file will be discussed in the solutions below.- For relevant issues / PRs on how these made their way in:
- A lot of older posts and StackOverflow answers will suggest things like copying and pasting code out of a Gist, using
- There are some big open issues with Web Worker types that are blocking easier solutions for strong-typing service worker files.
- These will be discussed again, throughout this post, but the major ones are:
Solution A: Triple Slash Directives
Since a Service Worker often lives in a single file, some users prefer to pull in its lib types via a triple slash directives instead of modifying the tsconfig
.
/// <reference no-default-lib="true"/>
/// <reference lib="ES2015" />
/// <reference lib="webworker" />
// Default type of `self` is `WorkerGlobalScope & typeof globalThis`
// https://github.com/microsoft/TypeScript/issues/14877
declare var self: ServiceWorkerGlobalScope;
// Example:
self.addEventListener('install', () => {
//
});
// We need an export to force this file to act like a module, so TS will let us re-type `self`
export default null;
lib=ES2015
should be changed to whatever you are using, but it should be an es5+ choice
If you have noticed that using triple-slash directives to bring in Web Worker types in your service worker file also messed up your regular TS files, you are not imagining things. For a pure TS solution, skip to solution B, which explores multiple scoped TSConfigs.
But, for JS users, we can bypass this tricky issue; read on below!
Triple-Slash Directives with JavaScript and JSDoc
Here is the same approach as above, but carried out with JavaScript, using JSDoc for types:
// @ts-check
/// <reference no-default-lib="false"/>
/// <reference lib="ES2015" />
/// <reference lib="webworker" />
// Using IIFE to provide closure to redefine `self`
(() => {
// This is a little messy, but necessary to force type assertion
// Same issue as in TS -> https://github.com/microsoft/TypeScript/issues/14877
// prettier-ignore
const self = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (globalThis.self));
self.addEventListener('install', (evt) => {
// ...
});
})();
💡 Ah-ha! In implementing this solution I have found a trick around the issue of triple-slash directives polluting other files.
The trick is to exclude the JS file from your tsconfig
and then add back checking by using //@ts-check
at the top. This prevents the triple slash directives in the file from “bleeding” into the other files that are not excluded by the tsconfig. This will prevent things like window
becoming undefined or all instances of globalThis.self
getting changed to the Service Worker Type!
lib=ES2015
should be changed to whatever you are using, but it should be an es5+ choice
Solution B: TSConfig Libs
Triple slash directives are not always the best approach, especially since they can “bleed” into other files anyways when you are using them with libs (they essentially act like global lib options – see docs). So, there is not much benefit over using the --lib
flag or lib
option in tsconfig
.
Relevant issue: #20595
In fact, the best option might be to have a different tsconfig
for your service worker, versus for all other files. For example:
Base tsconfig.json
:
{
"compilerOptions": {
"target": "ES6"
},
"exclude": ["service-worker.js"]
}
Service-Worker tsconfig.service-worker.json
:
{
"extends": "./tsconfig",
"compilerOptions": {
"lib": ["WebWorker", "ES2015"]
},
"files": ["service-worker.js"]
}
If you are looking for a more detailed breakdown of this approach, check out this StackOverflow answer.
Nested Service Worker Folder
The above option works if you are only using TypeScript via CLI, and not for Intellisense, as VSCode does not resolve more than one tsconfig per folder. If you wanted to use the above solution with VSCode and get full in-IDE type-checking, you would need to move service-worker.js
to a sub-directory, with its tsconfig
file located alongside it.
However, it should be noted that this causes its own issue, and in fact, a big one. Service workers by default are granted a scope based on file location. So a worker located in the root directory:
.
├── index.html
├── tsconfig.json
└── service-worker.{ts|js}
Can access any request. However, if we move to a subfolder:
.
├── index.html
├── tsconfig.json
└── sw/
├── service-worker.{ts|js}
└── tsconfig.json
… now our service worker has a default scope of /sw
, and declaring a higher scope actually requires configuring our server to send a unique HTTP header with any request for our worker script!
The easiest fix is probably to use outDir
or outFile
to make sure that the transpiled SW script ends up in the hosted project root, regardless of the source code directory structure.
Finally a solution! Thank you!