webpack-contrib/worker-loader

[Docs] Typescript Usage

Closed this issue ยท 16 comments

The documentation for worker-loader here

https://webpack.js.org/loaders/worker-loader/

Has a section at the end titled 'Integrating with Typescript'. That has sections showing how to create a typings/custom.d.ts file, and how to import into App.ts.

However it does not show how the web worker should be defined in Typescript.

Also the code in the other examples won't work in a Typescript file as self.postMessage({ result: 'Fred'}) won't transpile in Typescript.

It seems the function signature for postMessage in Typescript is different from what works in Javascript.

Can someone complete the documentation with a functioning Typescript example please.

If it's any help, this is what I've been doing.

// src/MyWorker/worker.ts

const ctx: Worker = self as any;

ctx.onmessage = event => {
    // ...

    ctx.postMessage(/* response */);
};
// src/MyWorker/custom.d.ts

declare module 'worker-loader!*' {
    class WebpackWorker extends Worker {
        constructor();
    }

    export = WebpackWorker;
}
// src/MyWorker/index.ts

import './custom'; // Not necessary if you import your custom.d.ts elsewhere
import MyWorker = require('worker-loader!./worker');

export default MyWorker;

And then I import and use the worker like any other module

// src/index.ts

import MyWorker from './MyWorker';

const myWorker = new MyWorker();

myWorker.onmessage = event => {
    // ...
};

myWorker.postMessage(/* ... */);

My actual implementation uses subclasses of Worker so I can add type safety to my events as well, but that might not be necessary for a basic usage example. The general idea is just that I use a custom event interface that extends MessageEvent, and set its data field to be a union type of all possible events. Each event must have a type field set to an enum value, then I can narrow it to the specific event type using a switch statement.

@grind086 Mind spinning up a PR to add this example to the docs ? Please as minimal && generic as possible and intead of // src/index.ts (Comment) =>

*index.ts*
```ts
// Code here ...
```

index.ts

// Code here ..

@grind086 Do you then use guard functions to differentiate between message types?
Do you use a string as type? Or shape of properties?

@michael-ciniawsky Done in #97, let me know if I should change anything.

@qm3ster I use a string enum for types, but a regular enum would work just as well. Shape of properties would work too, but might require a bit more involved type checking than a simple switch. It also wouldn't let you differentiate between events that have the same shape, though that might not be necessary anyway. Here's a full example of my current setup:

First define all of your message types

// MyWorker/types.d.ts

// Enumerate message types
export const enum MESSAGE_TYPE {
    READY: 'ready',
    REQUEST: 'request',
    RESULT: 'result',
    ERROR: 'error'
}

// Define expected properties for each message type
interface IReadyMessage {
    type: MESSAGE_TYPE.READY;
}

interface IRequestMessage {
    type: MESSAGE_TYPE.REQUEST;
    paramA: string;
    paramB: number;
}

interface IResultMessage {
    type: MESSAGE_TYPE.RESULT;
    data: Float32Array;
}

interface IErrorMessage {
    type: MESSAGE_TYPE.ERROR;
    error: string;
}

// Create a union type of all messages for convenience
type MyWorkerMessage = IReadyMessage | IRequestMessage | IResultMessage | IErrorMessage;

// Extend MessageEvent to use our messages
interface IMyMessageEvent extends MessageEvent {
    data: MyWorkerMessage;
}

// Extend Worker to use our custom MessageEvent
export class MyWorker extends Worker {
    public onmessage: (this: MyWorker, ev: IMyMessageEvent) => any;

    public postMessage(this:  MyWorker, msg: MyWorkerMessage, transferList?: ArrayBuffer[]): any;
    public addEventListener(type: 'message', listener: (this: MyWorker, ev: IMyMessageEvent) => any, useCapture?: boolean): void;
    public addEventListener(type: 'error', listener: (this: MyWorker, ev: ErrorEvent) => any, useCapture?: boolean): void;
}

Then write a worker as above, but use your worker's interface instead of the generic one.

// MyWorker/worker.ts

import { MyWorker, MESSAGE_TYPE } from './types';

const ctx: MyWorker = self as any;

ctx.onmessage = event => {
    const msg = event.data;

    switch (msg.type) {
        case MESSAGE_TYPE.REQUEST:
            const paramA = msg.paramA; // string
            const paramB = msg.paramB; // number

            try {
                const data: Float32Array = getResultSomehow(paramA, paramB);
                ctx.postMessage({ type: MESSAGE_TYPE.RESULT, data });
            } catch (e) {
                ctx.postMessage({ type: MESSAGE_TYPE.ERROR, error: e.message });
            }
            return;
    }
};

ctx.postMessage({ type: MESSAGE_TYPE.READY });

I like to wrap the whole thing up in a simple module like so

// MyWorker/index.ts

import { MyWorker } from './types';
import MyWorkerImport = require('worker-loader!./worker');

export { MESSAGE_TYPE } from './types';
export default MyWorkerImport as typeof MyWorker;

And then use it like this

// index.ts

import MyWorker, { MESSAGE_TYPE } from './MyWorker';

const worker = new MyWorker();
let isReady = false;

worker.onmessage = event => {
    const msg = event.data;

    switch (msg.type) {
        case MESSAGE_TYPE.READY:
            isReady = true;
            return;
        case MESSAGE_TYPE.RESULT:
            doSomethingWith(msg.data);
            return;
        case MESSAGE_TYPE.ERROR:
            console.error(msg.error);
            return;
    }
};

function makeRequest(paramA: string, paramB: string) {
    if (!isReady) {
        throw new Error('The worker is still loading!');
    }

    worker.postMessage({ type: MSG_TYPE.REQUEST, paramA, paramB });
}

I didn't bother with differentiating between messages meant to go from or to the worker, but if you wanted you could certainly split them up.

I see. Very similar to what I was doing. Enums might be the way to go, as differentiating by shape forced me to use "guard functions", it didn't just refine into a specific union member by doing if (msg.property) like it did in flow.

flq commented

So, really, I'm not getting this to fly at all and the only thing I can come up with that could be different is the webpack.config - how does it look like in the simple scenario outlined in this issue?

dakom commented

The main docs did not work for me - but @grind086 's solution does!

Specifically, this fails (from the Readme):

import Worker from 'worker-loader!./Worker';

And this works:

import Worker = require('worker-loader!./Worker');

Using import and require feels awkward... can someone please explain what's going on? :)

Also - I ended up just casting the worker to any like const worker:Worker = new (MyWorker as any)();, which isn't the end of the world... but just putting that out there ;)

@dakom - That's because apparently someone changed the official docs without understanding why they were the way they were. When you have a module that exports a single object (ie module.exports = ...) you import it with import ... = require('...'). This is the code structure worker-loader uses, and the current docs are incorrect.

EDIT: Here's a link to the relevant TypeScript docs - https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require

Is there an example where you can just import worker from 'worker' (so without the !worker-loader) where the custom.d.ts is centrally loaded and that your webpack loader config is more in this style, just like how it works in the js version?

{
	test: /\.worker\.ts$/,
	use: [
		{
			loader: 'worker-loader',
			options: { name: '[name].[hash].js' }
		},
		{
			loader: 'babel-loader',
			options: {
				cacheDirectory: true
			}
		}
	]
}

I got this working with the plain .worker.ts filename convention. Not a fantastic solution, but you can add this line to the bottom of your .worker.ts file:

// Expose the right type when imported via worker-loader.
export default {} as typeof Worker & {new (): Worker};

This makes it so, from a type system perspective, the other side imports it as the sort of Worker class exposed by worker-loader. Unfortunately, it also means that the worker file now exports an empty object {}, which is weird but doesn't cause any problems.

I'd recommend alway setting "esModuleInterop": true in your tsconfig.json, which makes it so import A from 'A' and import A = require('A') are the same, so you can always use regular import syntax. This puts the import behavior in-line with Babel, Webpack, and Node.js rather than the legacy TypeScript approach, and the only reason it's not enabled by default is to avoid breaking older projects. More details here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#support-for-import-d-from-cjs-form-commonjs-modules-with---esmoduleinterop

It seems to work even if const ctx: Worker = self as any; is written as declare const self: Worker;.

Is there an example where you can just import worker from 'worker' (so without the !worker-loader) where the custom.d.ts is centrally loaded and that your webpack loader config is more in this style, just like how it works in the js version?

{
	test: /\.worker\.ts$/,
	use: [
		{
			loader: 'worker-loader',
			options: { name: '[name].[hash].js' }
		},
		{
			loader: 'babel-loader',
			options: {
				cacheDirectory: true
			}
		}
	]
}

is it support that now?

Yes

it looks well~~but the publicPath option doesn't work. equal default webpackConfig.output.publicPath value.
i use worker-loader@3.0.2 & webpack@4.x

i use inlined loader

import Worker from 'worker-loader?filename=[name].[hash:8].js&publicPath=./test/!./worker';

or webpack config:

{
	test: /\.worker\.ts$/,
	use: [
		{
			loader: 'worker-loader',
                        publicPath: './test/',
			options: { name: '[name].[hash].js' }
		},
		{
			loader: 'babel-loader',
			options: {
				cacheDirectory: true
			}
		}
	]
}

both of them can't find /test/ on the result of the compiled file.

I find the source code https://github.com/webpack-contrib/worker-loader/blob/master/src/utils.js#L84, is that means the publicPath options is never use? It seems to a difference from the file-loader's publicPath option.

{
			loader: 'worker-loader',
			options: { name: 'test/[name].[hash].js' }
		},

Don't touch publichPath.

You are confusing file location and publicPath. Webpack@5 will have publicPath: auto and we will recommend do not touch publicPath.

{
			loader: 'worker-loader',
			options: { name: 'test/[name].[hash].js' }
		},

Don't touch publichPath.

You are confusing file location and publicPath. Webpack@5 will have publicPath: auto and we will recommend do not touch publicPath.

ok, thanks for your reply.