Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specifying lib: DOM and WebWorker should not be mutually exclusive #20595

Open
armandn opened this issue Dec 9, 2017 · 24 comments
Open

Specifying lib: DOM and WebWorker should not be mutually exclusive #20595

armandn opened this issue Dec 9, 2017 · 24 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@armandn
Copy link

armandn commented Dec 9, 2017

I got myself into a situation similar to #11093.

My project is designed to work either in the "main thread", with access to DOM, or as a WebWorker, where a different behavior kicks in. This was done for compatibility reasons and to have only one distributable js file and it's been working fine for the last two years.

In all this time I never specified the lib compiler option, only target ES5.

Today, as I'm experimenting some ES2015 stuff, I see that if I specify targets DOM,ES5,WebWorker,ES2015.Promise all hell breaks loose with errors. - Duplicate identifier, Subsequent variable declarations must have the same type.

Shouldn't this kind of mix be allowed in a project?

@DanielRosenwasser
Copy link
Member

Unfortunately the base APIs for WebWorker and DOM are actually meaningfully different, and it's not clear at this point how to write a clean separation of global scopes between different parts of your project other than splitting your project into multiple parts.

@DanielRosenwasser DanielRosenwasser added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Dec 12, 2017
@kitsonk
Copy link
Contributor

kitsonk commented Dec 12, 2017

We have found that what is meaningful in the global scope of a web worker is significantly more narrow than we find out of the DOM. So we tend to author with "lib": [ "dom" ] and then just declare the handful of APIs we require to make a web worker work, or we obtain a reference to the global object, treat it as any, and pick off certain properties from the global scope and type them at the point where we pick them off.

While it isn't ideal, it saves us from the only other realistic option, splitting the code. If you have modular code, and pick off parts of a global scope and type them, and you are using modular code, you should be able to author isomorphic modules fairly easily. Here is an example of the most "bullet proof" way to get a reference to the global scope, which avoids any issues with CSP:

const globalObject: any = (function (): any {
	if (typeof global !== 'undefined') {
		// global spec defines a reference to the global object called 'global'
		// https://github.com/tc39/proposal-global
		// `global` is also defined in NodeJS
		return global;
	}
	else if (typeof window !== 'undefined') {
		// window is defined in browsers
		return window;
	}
	else if (typeof self !== 'undefined') {
		// self is defined in WebWorkers
		return self;
	}
})();

@shamhub
Copy link

shamhub commented May 18, 2018

@kitsonk But, do I need to write your above code in every file? and use globalObject for referring properties
How do I handle this scenario in here?

@charlesbodman
Copy link

charlesbodman commented May 18, 2018

@shamhub , no, you would use the global object instead of self

import globalObject from 'globalObject'

globalObject.postMessage({});

That being said, in your case, you don't need the global object. If you want to get around the typing errors, you can just use

const ctx: Worker = self as any;
ctx.postMessage();

@shamhub
Copy link

shamhub commented May 19, 2018

@charlesbodman I have used second option but I get error No webworker support as mentioned here

@charlesbodman
Copy link

@shamhub , it's because you're checking for worker support inside of the worker. No need.

@shamhub
Copy link

shamhub commented May 21, 2018

@charlesbodman Yes it works now. In JS, we have SharedWorker object. Am trying to execute the same code using let worker: SharedWorker = new SharedWorker('worker.js'); but I see the error Cannot find name 'SharedWorker'

@mhegazy
Copy link
Contributor

mhegazy commented May 21, 2018

SharedWorker needs to have its own lib file. mind filing a new ticket to track creating that.

@mhegazy
Copy link
Contributor

mhegazy commented May 22, 2018

logged #24323 to track adding SharedWorker support

@filipesilva
Copy link
Contributor

@mhegazy @DanielRosenwasser we're also running into the incompatible dom and webworker typings in Angular CLI projects and that leaves us in a tricky spot. I have described the situation in angular/angular-cli#14188 (comment).

I suppose the "right" approach would be for IDE support to better approximate compilation support by supporting multiple tsconfigs on the same folder that affect different combination of files. But that sounds hard and needs to be supported by the IDE proper as well.

I'm not sure what the correct approach is right now.

@DanielRosenwasser
Copy link
Member

(Heads up, @mhegazy is no longer on the team. CCing @RyanCavanaugh)

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Apr 18, 2019
@RyanCavanaugh
Copy link
Member

We should reorganize this a bunch

  • lib.browser.d.ts is things you can access from WW or DOM contexts
  • lib.window.d.ts is just the definition of Window
  • lib.window.globals.d.ts adds the existing arbitrarily-selected global values from Window
  • lib.webworkers.d.ts is browser + ww-specific stuff
  • lib.dom.d.ts is (for back compat) window + browser + window.globals

@jacobbogers
Copy link

Any progress on this? I am having the same issue

jahow added a commit to jahow/openlayers that referenced this issue Sep 23, 2019
The worker self object is simply cast to any to make sure typescript
does not complain.
Actual typecheck may be restored once the following issue is fixed:
microsoft/TypeScript#20595
@yGuy
Copy link

yGuy commented Jan 13, 2020

There is a related problem that library developers (typings/d.ts file providers) are facing. For libraries that actually work well in both the DOM and the webworker contexts, I don't see how it's possible to program against such a library in a webworker context, because whenever the library (only the d.ts file! not the source to be compiled!) contains a reference to a DOM type (like HTMLElement), the compiler will bail out with a TS2304, stating that the type cannot be found, even if the respective API is not used at all in the code to be compiled.

The only workaround I can see is to target "DOM" inside the webworker code (and declare all the webworker APIs that you need to use in order to make it compile) or to patch all the typings files and generate separate versions for use in webworkers, only. Although feasible, I don't like those workarounds. I would rather see the compiler be more lenient with respect to unused (with respect to the sources to are being compiled) declarations in a d.ts file.

@yGuy
Copy link

yGuy commented Jan 16, 2020

To add to my previous comment above: For libraries, there actually is a work around: It's also an all-or-nothing flag, so really having more control over this would be appreciated: Use the skipLibCheck option to disable checking of the .d.ts files that only compile cleanly in the "other" lib context.

@TroyAlford
Copy link

Though it is a little bit ugly, here is a method I've gotten to work:

(self as unknown as Worker).postMessage(/* message */)

This actually makes the intellisense work on the postMessage correctly as well in VSCode, by correctly forcing recognition of the type of self. I dislike the as unknown in the middle, but without that, I get a different TS error:

Conversion of type 'Window & typeof globalThis' to type 'Worker' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

If there's a cleaner syntax for this, I'd love a hint. :)

@sandersn
Copy link
Member

sandersn commented May 6, 2020

@orta and I put our heads together on this to see if @RyanCavanaugh's proposed solution is workable. So far, we think Yes, with some caveats that we need to investigate. Here's are some notes; we'll work more on this soon.

  1. The core problem is that people need to include webworker at the same time as DOM, but several of the declarations conflict. (16 of them, currently).
  2. When you look at the difference between webworker and DOM, the differences fall into 3 buckets:
    a. New types and values specific to webworker. We believe this is most of what people want from including webworker.
    b. Missing values and types that don't make sense for webworker. eg webworker doesn't have the type HTMLOrSVGImageElement. This is nice to exclude but not the highest priority.
    c. Types that conflict and values with conflicting types.
  3. @RyanCavanaugh's proposal to create a new dependent, lib.browser.d.ts, would solve (a) and (b) -- just put the core things into browser and let webworker or dom respectively extend the core.
  4. The conflicts in webworker could be fixed in the following ways
  • A few are just out of date, like DOMMatrixReadOnly, CrytoKeyPairs
  • A few, like FormData's constructor or ImageBitmapRenderingContext.canvas, could extend a base in browser in an unsound way.
  • Callbacks like onlanguagechange, onoffline, ononline, onrejectionhandled, onunhandledrejection, could be made compatible with the DOM by removing the this parameter.
  • This leaves onerror and self.
  • self is defined in the same way for both files, but webworker uses the much smaller type WorkerGlobalScope instead of Window. If it used the name Window instead it would probably merge cleanly with the DOM's self, although I haven't tested this yet.
  • onerror has a this parameter for webworker, and doesn't allow strings as event names, nor does it have 4 trailing optional parameters. But it might be worthwhile to change it if it's the only conflict left.
  1. With the conflicts resolved, people would be able to have both dom and webworker for their lib.
  2. All these types are built in the types exported from Edge, so changing them would require a lot of work in TSJS-lib-generator, manually converting the webidl declarations into the JSON override format. So testing this change will be labour-intensive.

@fatcerberus
Copy link

Having just discussed this on Gitter, I realized that the general problem here isn't just about dom and webworker, despite appearances; that's just a symptom. JS is ubiquitous nowadays and I'd expect it's not uncommon to have code for a few different platforms existing within the same source tree. But you only get one tsconfig.json (for the purposes of IDE support and IntelliSense, at least), and lib is global.

I actually have a similar problem to this today with miniSphere: the vast majority of a game will use the Sphere typings, but the build script--Cellscript.js uses the Cell API, which like Web Workers vs. DOM, is generally distinct but shares a few select classes in common.

So if there were a way to use separate typings per-file or even just per-directory, that would be an elegant solution to this problem, I think, and probably wouldn't even require modifying the .d.ts files as they're currently written.

@cecilemuller
Copy link

cecilemuller commented Jan 20, 2021

In the meantime, the workaround I use is typing self in the worker file:

/* eslint-env worker */
const worker: Worker = self as any;

worker.addEventListener("message", (e) => {
  worker.postMessage({hello: "world"});
});

You can even go further and type the messages:

worker.ts

/* eslint-env worker */

type TRequest = {
  myrequest: number;
};
type TResponse = {
  myresponse: number;
};

export interface IRequestWorker {
  postMessage: (message: TRequest) => void;
  onmessage: (message: MessageEvent<TResponse>) => void;
}
interface IResponseWorker {
  postMessage: (message: TResponse) => void;
  onmessage: (message: MessageEvent<TRequest>) => void;
}

const worker: IResponseWorker = self as any;

// Receive from the main thread
worker.onmessage = ({data: {myrequest}}) => {
  console.log(myrequest);

  // Send to the main thread
  worker.postMessage({myresponse: 222});
};

main.ts

/* eslint-env browser */
import type {IRequestWorker} from "./worker.ts";

const worker = new Worker(new URL("./worker.ts", import.meta.url)) as IRequestWorker;

// Receive from the worker
worker.onmessage = ({data: {myresponse}}) => {
  console.log(myresponse);
};

// Send to the worker
worker.postMessage({myrequest: 111});

export {};

@cameron-martin
Copy link

cameron-martin commented Oct 28, 2021

Having just discussed this on Gitter, I realized that the general problem here isn't just about dom and webworker, despite appearances; that's just a symptom. JS is ubiquitous nowadays and I'd expect it's not uncommon to have code for a few different platforms existing within the same source tree. But you only get one tsconfig.json (for the purposes of IDE support and IntelliSense, at least), and lib is global.

I actually have a similar problem to this today with miniSphere: the vast majority of a game will use the Sphere typings, but the build script--Cellscript.js uses the Cell API, which like Web Workers vs. DOM, is generally distinct but shares a few select classes in common.

So if there were a way to use separate typings per-file or even just per-directory, that would be an elegant solution to this problem, I think, and probably wouldn't even require modifying the .d.ts files as they're currently written.

This issue also manifests its self when using test frameworks that declare globals. It makes these globals available in each file even though they are not at runtime. It also means when you use multiple test frameworks that each declare globals of the same name, you get conflicting global definitions.

Having a mechanism for specifying different types for different file patterns sounds like a good solution to me.

@capnmidnight
Copy link

As a stop-gap, it would be nice if there were a library called "DOMAndWebWorker" that was the intersection of the "DOM" and "WebWorker" libraries.

But to solve the problem in general, we can see from the fact that including "DOM" and "WebWorker" in compilerOptions.lib results in an OR operation between the two libraries, that what we need is some way to do AND operations between libraries. Maybe the compilerOptions.lib property could be set as a string, something like "ESNext | (DOM & WebWorker)".

aarongable pushed a commit to chromium/chromium that referenced this issue Feb 28, 2023
This CL does a couple of things:

1. Moves the worker script into its own ts_library because it needs
   a different tsconfig.json than the rest of the code. Ideally we would
   be able to use the same target for both, but declarations in the
   lib.webworker.d.ts conflict with lib.dom.d.ts. See
   microsoft/TypeScript#20595

2. The tsconfig.json needs 'webworker' for web worker APIs.

3. In the worker script, add an empty export so we can re-declare
   `self` and re-declare `self` as SharedWorkerGlobalScope so we can
   use shared worker APIs. See
   microsoft/TypeScript#14877

4. Fixes shared worker creation to work with trusted types. We loose
   some type safety when we create the worker because lib.dom.d.ts
   doesn't support Trusted Types yet. See
   microsoft/TypeScript#30024

5. Random fixes for the linter and typescript compilation.

Change-Id: I1d21e67f58b5f3d6b879c21af9e18c18aa6025fd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4289336
Reviewed-by: Zain Afzal <zafzal@google.com>
Commit-Queue: Giovanni Ortuno Urquidi <ortuno@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1110774}
@muhamedkarajic
Copy link

So I guess I'm here after 6 years just wondering if there is some workaround cause all I would like is execute my classes and functions inside the webworker but in a typescript nice way. Is there a solution for that if I have e.g. a module and then want to import things from that module inside my worker?

All posts I saw seemed to be workarounds by waiting for webpack to compile it and then grab a specific module etc.

@cuixiping
Copy link

cuixiping commented Apr 12, 2023

If one file need work on both dom and web worker, how to config the compilerOptions.lib?

For example:

if (typeof importScripts == 'function' && typeof window=='undefined') {
   // some code for web worker
  importScripts('abc.js');
} else {
  // some code for dom
  alert(document.title);
}

egasimus added a commit to hackbg/cosmjs-esm that referenced this issue Aug 10, 2023
@MattiasMartens
Copy link

If one file need work on both dom and web worker, how to config the compilerOptions.lib?

The current state seems to be that there is no correct type-safe way to do this.

The only available option for module authors is to maintain two modules, one compiled for web worker and one compiled for DOM, and then a "dispatcher" module that imports both as un-typed javascript, uses a non-type-safe environment tester function to determine which one to call, and dispatches module consumer function calls to one or the other. That dispatcher module can then type itself, without referencing either "dom" or "webworker" definitions, and hide the kludge from consumers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests