Skip to content

Allow a union of libraries, for code that runs in multiple environmentsΒ #52433

Open

Description

Suggestion

πŸ” Search Terms

webworker, dom, node, isomorphic

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Sometimes code is designed to run in multiple environments. For example, code may run in both nodejs and the browser, or a web page and a web worker.

Currently, you can use tsconfig to say your environment is both a 'DOM' and 'webworker', but that's never true in the wild. It's 'DOM' or 'webworker'.

Take this example (playground link):

localStorage.set('foo', 'bar');
importScripts('hello.js');

TypeScript is fine with this if you include both 'DOM' and 'webworker' libs, but this code will fail in a webworker, because localStorage doesn't exist, and it will fail in a page, because importScripts doesn't exist.

This would be solved by a feature that allows developers to specify the environment as A or B. The code above would show errors, because localStoage and importScripts may not exist.

πŸ“ƒ Motivating Example

Hopefully the above description provides this.

πŸ’» Use Cases

Hopefully the above description provides this.

Activity

  1. MartinJohns commented on Jan 26, 2023

    @MartinJohns
    Contributor

    You can already have multiple tsconfig and change them in VSCode (or editor of your choice). Otherwise it sounds like a duplicate of #37884.

  2. jakearchibald commented on Jan 26, 2023

    @jakearchibald
    Author

    Having to switch configs is a bad developer experience here. Although, if the editor performed the checking with multiple configs and presented a union of the errors, that might satisfy this feature request.

    Although, a single config that knows that the types are DOM or webworker would be faster than running two checks.

  3. oliverdunk commented on Jan 26, 2023

    @oliverdunk

    To take the example a bit further, imagine wanting to write this and tell developers it is safe to run in both workers and normal scripts:

    if ("localStorage" in globalThis) {
      localStorage.set('foo', 'bar');
    }
    

    This should compile. localStorage doesn't exist in a worker, but this code is fine because of the condition check.

    Right now, for this code to build you have to include the DOM library. But as soon as you do that, TypeScript will happily let you remove the condition and write potentially unsafe code!

  4. RyanCavanaugh commented on Jan 26, 2023

    @RyanCavanaugh
    Member

    The problem we've had here in the past is that a huge amount of code can be seen to be running in one environment by inspection, but not in a way that's susceptible to static analysis. An example would be something like writing

    if ("document" in globalThis) {
      window.addEventListener("click", () => {
        localStorage.set("foo", "bar");
      }
    }

    This code is obviously fine, but TS would think that window and localStorage are possibly-not-present because there's no linkage between one DOM thing being present and everything else.

    There's nothing today stopping someone from auto-genning variant DOM/webworker/etc libraries with every top-level declaration marked possibly-undefined. Real-world usage of such output would be good evidence that this kind of feature would be usable in practice.

  5. jakearchibald commented on Jan 26, 2023

    @jakearchibald
    Author

    Ryan Cavanaugh (@RyanCavanaugh) in those cases, I think it's reasonable to put a type guard on the global object, then access the APIs via the global object.

    Although, typescript could be smart and realise that if the type of globalThis is guarded, global access could reflect that.

  6. jakearchibald commented on Jan 27, 2023

    @jakearchibald
    Author

    As in:

    function globalIsWindow(global: any): global is Window {
      return 'document' in global;
    }
    
    if (globalIsWindow(self)) {
      self.addEventListener("click", () => {
        self.localStorage.set("foo", "bar");
      });
    }

    But if TypeScript could determine that self represents the global-this, then:

    if (globalIsWindow(self)) {
    -  self.addEventListener("click", () => {
    +  addEventListener("click", () => {
    -    self.localStorage.set("foo", "bar");
    +    localStorage.set("foo", "bar");
      });
    }
  7. davidbarratt commented on Jul 7, 2023

    @davidbarratt

    Perhaps this is what Jake Archibald (@jakearchibald) is saying, but I noticed that predicates on globalThis don't work:
    https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABDAzgdRmAJnA7gCkwAcQoAuRcAazDzAEoLjTkVENs9EBvAKEUQAnAKZQQgpFACeRYXGDIwJKIgC86xAHI4AIwBWw6JsQAyE4uWIAhBrAgANvdPnNOCCAC2wsFGOYLpADcvAC+vLwwCvhWqBw4BADm9roAhvYAKgAWqPT0PPyISakZ2SgAdG6e3lBlKVhYAKIAbtUAMqhQ3sKC+JrAcO4omgA0iPh5qgB8+QUh9MEhQA

    If that could work, then it could be simplified to something like this:

    function isClient(): globalThis is Window {
      return 'document' in globalThis;
    }

    where global keywords can be used in predicates.

  8. robbiespeed commented on Jul 7, 2023

    @robbiespeed

    Something like #50424 would make it a lot easier to create these kinds of isomorphic type guards, without needing to fork global type libs as Ryan Cavanaugh (@RyanCavanaugh) suggests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions