[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.
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?
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
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.