Alright, let’s talk about typing your Service Worker. You’ve written your sw.js, it’s caching assets like a champ, but now you’re staring at your TypeScript code and getting a bunch of angry red squiggles. self is of type Window, and caches doesn’t exist? Madness. This is where @types/serviceworker waltzes in, not as a hero, but as a very competent stagehand who finally turns the lights on so you can see what you’re doing.

This package isn’t some magical polyfill; it doesn’t add functionality. The browser’s runtime provides the Service Worker API. What this type package does is far more fundamental: it tells TypeScript what the heck that runtime environment actually looks like. It slaps a proper type on self, defining all the events, caches, and clients you expect, shutting the compiler up and giving you glorious, glorious autocomplete.

The Core Concept: Replacing self’s Type

In a normal web window, self is a Window. In a Service Worker’s execution context, self is a ServiceWorkerGlobalScope. This is the root of all your typing problems. The @types/serviceworker package fixes this by using TypeScript’s ambient context magic. Once you install it (npm install -D @types/serviceworker), it automatically declares that within a Service Worker context, self is no longer a Window but a ServiceWorkerGlobalScope.

This means you can just start typing. Create a service-worker.ts file (or .js if you’re not compiling), and witness the beauty:

// This code knows about the Service Worker environment thanks to @types/serviceworker
self.addEventListener('install', (event: ExtendableEvent) => {
  // event.waitUntil is now a recognized method!
  event.waitUntil(
    caches.open('my-cache-v1').then((cache) => {
      // cache.addAll is also properly typed!
      return cache.addAll(['/', '/styles/main.css', '/script/app.js']);
    })
  );
});

self.addEventListener('fetch', (event: FetchEvent) => {
  // Yep, FetchEvent is a known type, and so is event.respondWith.
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

See? No explicit import. The types are injected globally because, just like the actual API, they are global in this context. It just works.

Handling Service Worker Registration in Your App Code

Here’s a common trip-up. Your application code, which lives in the normal window context, needs to register the service worker. This uses the navigator.serviceWorker API, which is part of the main browser types. You don’t need @types/serviceworker for this part. In fact, you shouldn’t use it here. This keeps things cleanly separated.

// app.ts - Your main application code
// This is running in the Window context.

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw.js') // Register your compiled Service Worker file
    .then((registration: ServiceWorkerRegistration) => {
      console.log('SW registered: ', registration);
    })
    .catch((registrationError: Error) => {
      console.log('SW registration failed: ', registrationError);
    });
}

The key difference? We’re using navigator.serviceWorker here, not the self from inside the worker. The types for ServiceWorkerRegistration and others are already in @types/node or lib.dom.d.ts.

Taming Events and Avoiding the any Trap

The most significant benefit is with events. The Service Worker lifecycle is event-driven, and each event has very specific properties. Without proper types, it’s tempting to just use any for the event parameter. Don’t. You’ll miss things.

With @types/serviceworker, you get precise types for every event:

  • InstallEvent: Actually an ExtendableEvent, with waitUntil.
  • FetchEvent: Has request and respondWith.
  • PushEvent: Has data for handling push messages.
  • SyncEvent: Has a tag for background sync.

This precision prevents bugs. The compiler will now yell at you if you try to call event.respondWith inside an install event, which is exactly what it should do.

The One Quirk: Explicitly Using ServiceWorkerGlobalScope

Sometimes, for clarity or to access a property that might be shadowed, you might want to explicitly reference the global scope type. You can import it directly from the package. This is its one explicit use case.

// Inside your service-worker.ts
import type { ServiceWorkerGlobalScope } from 'serviceworker';

// Type assertion can be useful for clarity or in complex scenarios
const swSelf = self as unknown as ServiceWorkerGlobalScope;

swSelf.skipWaiting(); // Methods are now available on the explicitly typed variable

Honestly, you’ll rarely need this. The automatic typing of self covers 99% of use cases. Consider this a tool for your belt if you run into a weird scoping issue or need to pass the global scope around for some reason.

In short, @types/serviceworker is non-negotiable. It turns a confusing, error-prone development experience into a structured and guided one. It’s the difference between fumbling in a dark room for a light switch and just knowing where the door is. Install it. Use it. Thank yourself later.