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

Expose global declarations on window #19816

Closed
mhegazy opened this issue Nov 7, 2017 · 23 comments · Fixed by microsoft/TypeScript-DOM-lib-generator#715
Closed

Expose global declarations on window #19816

mhegazy opened this issue Nov 7, 2017 · 23 comments · Fixed by microsoft/TypeScript-DOM-lib-generator#715
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@mhegazy
Copy link
Contributor

mhegazy commented Nov 7, 2017

Window in a web page serves a dual purpose. it implements the Window interface representing the web page main view, but also acts as an alias to the global namespace. All global variables are accessible on the window object at run-time; this applies to builtin JS declarations like Array, Math, JSON, Intl as well as global DOM declarations like HTMLElement, Event, HTMLCollection, etc...

TypeScript today does not model this behavior. For instance window.Math is not defined, so is all the HTMLElement family of declarations.

Most of the patterns window is used to access global declarations are anti-patterns (see a good article about window usage), the main exception is testing for API existence, e.g.: if (window.MutationObserver) { ... }. There is not really a legal way to do this out-of-the-box in TS today.

Our recommendation has been to manually add a declaration for the global object on Window, e.g.:

declare global {
    interface Window {
        MutationObserver?: typeof MutationObserver;
   }
}

This is a. inconvenient and b. not easy to find if you are new to TS. Here is a list of issues we have so far related to this issue:

It is also worth noting that the same problem occurs with self in web workers and global for node

Possible options here:

  1. support a global type (tracked by Access global as a type #14052), and allow Window to extend from it.
  2. generate lib.d.ts with all global declarations mirrored on Window.
@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Nov 7, 2017
@mhegazy mhegazy changed the title Expose global declaration on window Expose global declarations on window Nov 7, 2017
@yortus
Copy link
Contributor

yortus commented Nov 8, 2017

Can I bring up a third option? I think option 1 is the more flexible way of reducing duplicate declarations. However both options 1 & 2 have limitations:

  1. supporting a global type and having Window extend it is pretty good because it is simple, and other environments (eg Global in node.d.ts) can use this too. But it would not solve the duplication problem in custom sandboxed environments, whereas the equally simple option 3 (below) would solve it.

  2. generating lib.d.ts with all globals mirrored on Window - but then do other environments miss out? node.d.ts has similar duplication of declared globals with the Global interface, and then there are web workers, custom environments, etc.

A third option would be a declaration that tells the compiler to augment the global environment with all the declarations in a particular interface. Eg declare global extends Window for lib.d.ts, declare global extends Global for node.d.ts, etc.

This is similar to option 1 but in reverse, and allows additional scenarios like sandboxing. An example of this scenario is given in #10050 (comment). Note that if option 1 was used, then there would be no way to write a set of declarations that are just a normal interface in the host code but are are globals in the sandboxed code. They would all still have to be duplicated.

Custom environments may be niche, but it would be nice if the mechanism chosen here would work for them too.

KevinSnyderCodes pushed a commit to KevinSnyderCodes/DefinitelyTyped that referenced this issue Nov 8, 2017
TypeScript team is figuring out how to expose these types on their `Window` interface, which `DOMWindow` extends (discussion [here](microsoft/TypeScript#19816)). In the meantime, this workaround will ensure that jsdom code compiles.

Some types are not defined even globally by TypeScript. These have been commented out in the `DOMWindow` interface, but are still valid properties in jsdom. If someone tries to use these properties, their code will still not compile.
KevinSnyderCodes pushed a commit to KevinSnyderCodes/DefinitelyTyped that referenced this issue Nov 8, 2017
TypeScript team is figuring out how to expose these types on their `Window` interface, which `DOMWindow` extends (discussion [here](microsoft/TypeScript#19816)). In the meantime, this workaround will ensure that jsdom code compiles.

Some types are not defined even globally by TypeScript. These have been commented out in the `DOMWindow` interface, but are still valid properties in jsdom. If someone tries to use these properties, their code will still not compile.
@michahell
Copy link

michahell commented Apr 9, 2018

window.Worker, same story...

@HolgerJeromin
Copy link
Contributor

HolgerJeromin commented Jul 23, 2018

Most of the patterns window is used to access global declarations are anti-patterns (see a good article about window usage),
the main exception is testing for API existence, e.g.: if (window.MutationObserver) { ... }. There is not really a legal way to do this out-of-the-box in TS today.

One other valid usecase is if you are working with different contexts. For example we could be in another DOM (inside an HTMLObjectElement) so SVGElement is a different one:

if ((myElement instanceof (myElement.ownerDocument.defaultView as any).SVGElement)) {
    // yeah, really a svg
}

@mpawelski
Copy link

I just hit on this when I tried to migrate js library that uses a lot of if(windows.Something) test.

I though that it should be fixed in TS 3.4 after #29332
However I see that window is still declared as declare var window: Window; not as Window & typeof globalThis as described in globalThis PR. @sandersn Are there plans to change it?

@sandersn
Copy link
Member

@mpawelski Yes, by 3.5. I just haven't had a chance yet.

@thomaLV
Copy link

thomaLV commented May 30, 2019

What is the status on this?

@maksnester
Copy link

maksnester commented Jul 5, 2019

Hey guys, so eventually, how nowadays with "typescript": "3.4.5" I can declare that inside window object, I have some variable with some type?

E.g. I have window.foo = { bar: 'baz' }, how I can declare it? Old way of doing that, I mean

declare global {
  interface Window {
    foo: { bar: string }
  }
}

is now produces this error

TS2669: Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. Alt+Shift+Enter Alt+Enter

UPD: it looks like just adding a record in some d.ts file is ok. Like this:

interface Window {
  foo: { bar: string }
}

But as I understood in such case I can't import some types in that file and use them in declaration, instead of string for example. So what if I want to do something like that:

import { MyTypeFromSomewhere } from './somewhere.d.ts'
interface Window {
  foo: { bar: MyTypeFromSomewhere }
}

?

@inoyakaigor
Copy link

@alendorff just to clarify: you had TS2669… error inside global.d.ts or index.d.ts?

@maksnester
Copy link

@inoyakaigor sorry, didn't understand your question, what's the difference between these 2 files if both are .d.ts ? I've not mentioned any specific filenames or something like that.

@inoyakaigor
Copy link

@alendorff I had the same TS2669 error for the code inside global.d.ts and I'm trying to figure out if the file name is relevant to this error. That's why I'm asking about the file name

@maksnester
Copy link

@inoyakaigor I don't have global.d.ts so filename is irrelevant I think

@TranquilMarmot
Copy link

For anyone else landing here from a search, the following works to define new things on window and will get rid of Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. ts(2669):

export {};

declare global {
  interface Window {
    // your types here!
  }
}

@mersanuzun
Copy link

I want the dynamic key in window object. How can I do this? For example
window.lastName or window.surname

Can I do this?

declare global {
  interface Window {
     [type: string]: any
  }
}

@raphael10-collab
Copy link

@TranquilMarmot

In a Electron-React-Typescript app I'm getting this error: Property 'api' does not exist on type 'Window & typeof globalThis'. window.api.send('open-type-A-window', '');

But in a file index.d.ts I declared interface Window in this way:

declare global {
  namespace NodeJS {
    declare interface Window {
      "electron": {
          openNewWindow(): void;
      },
      "api", {
          send: (channel, data) => {
              ipcRenderer.invoke(channel, data).catch(e => console.log(e))
          },
          receive: (channel, func) => {
            console.log("preload-receive called. args: ");
            ipcRenderer.on(channel, (event, ...args) => func(...args));
          },
          electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
            ipcRenderer.sendTo(window_id, channel, arg);
          },
          electronIpcSend: (channel: string, ...arg: any) => {
            ipcRenderer.send(channel, arg);
          },
          electronIpcSendSync: (channel: string, ...arg: any) => {
            return ipcRenderer.sendSync(channel, arg);
          },
          electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
            ipcRenderer.on(channel, listener);
          },
          electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) =>
 {
            ipcRenderer.once(channel, listener);
          },
          electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) 
=> void) => {
            ipcRenderer.removeListener(channel, listener);
          },
          electronIpcRemoveAllListeners: (channel: string) => {
            ipcRenderer.removeAllListeners(channel);
          }
      }
    }
  }
}

What should I add / modify in order to avoid this error Property 'api' does not exist on type 'Window & typeof globalThis' ?

  • node: v14.5.0
  • electron: v11.2.3
  • typescript: v4.1.3
  • OS: Ubuntu 18.04.4 Desktop

@TranquilMarmot
Copy link

@raphael10-collab I don't think you need the namespace NodeJS?

@raphael10-collab
Copy link

raphael10-collab commented Feb 9, 2021

@TranquilMarmot

I tried also without NodeJS :

index.d.ts :

declare global {
  //namespace NodeJS {
    //declare interface Window {
    interface Window {
      "electron": {
          openNewWindow(): void;
      },
      "api": {
          send: (channel, data) => {
              ipcRenderer.invoke(channel, data).catch(e => console.log(e))
          },
          receive: (channel, func) => {
            console.log("preload-receive called. args: ");
            ipcRenderer.on(channel, (event, ...args) => func(...args));
          },
          // https://www.electronjs.org/docs/all#ipcrenderersendtowebcontentsid-channel-args
          electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
            ipcRenderer.sendTo(window_id, channel, arg);
          },
          // https://github.com/frederiksen/angular-electron-boilerplate/blob/master/src/preload/preload.ts
          electronIpcSend: (channel: string, ...arg: any) => {
            ipcRenderer.send(channel, arg);
          },
          electronIpcSendSync: (channel: string, ...arg: any) => {
            return ipcRenderer.sendSync(channel, arg);
          },
          electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
            ipcRenderer.on(channel, listener);
          },
          electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => {
            ipcRenderer.once(channel, listener);
          },
          electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) => void) => {
            ipcRenderer.removeListener(channel, listener);
          },
          electronIpcRemoveAllListeners: (channel: string) => {
            ipcRenderer.removeAllListeners(channel);
          }
      }
    }
  //}
}

but still get this error:

src/app/components/App.tsx:92:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

92       window.api.send('open-type-A-window', '');
                ~~~

in /src/App.tsx :

if (win === "A-Type") {
  window.api.send('open-type-A-window', '');

@TranquilMarmot
Copy link

@raphael10-collab you have a typo:

"api", {

needs to be...

"api"{

Also, I'm not sure you can implement code in a .d.ts file like that. It is only meant for declaring types.

You should have a separate file that does something like this...

window.api = {
   send: (channel, data) => {
              ipcRenderer.invoke(channel, data).catch(e => console.log(e))
          },
   // etc.
}

@raphael10-collab
Copy link

After removing that typo: "api", { --------> "api": {
the problem persists.

So, may be it is like you say, that I have to define window.api = {} in a separate file.
But how to make it global? And in what kind of separate file? Any file?

I tried to put it into main.ts :

  window.api = {
      send: (channel, data) => {
          ipcRenderer.invoke(channel, data).catch(e => console.log(e))
      },
      receive: (channel, func) => {
        console.log("preload-receive called. args: ");
        ipcRenderer.on(channel, (event, ...args) => func(...args));
      },
      electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
        ipcRenderer.sendTo(window_id, channel, arg);
      },
      electronIpcSend: (channel: string, ...arg: any) => {
        ipcRenderer.send(channel, arg);
      },
      electronIpcSendSync: (channel: string, ...arg: any) => {
        return ipcRenderer.sendSync(channel, arg);
      },
      electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.on(channel, listener);
      },
      electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.once(channel, listener);
      },
      electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.removeListener(channel, listener);
      },
      electronIpcRemoveAllListeners: (channel: string) => {
        ipcRenderer.removeAllListeners(channel);
      }
  }

But still this error: TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

@TranquilMarmot
Copy link

@raphael10-collab

You will need to put it in a file and then import that file into your application somewhere (the best spot may be in your main application file, something like app.tsx?)

// window.ts
window.api = {
  // your code here
}

// app.tsx
import './window';

You will also still need the .d.ts file (I would suggest calling it window.d.ts) to declare the types that are on window. It will look something like this but with your types:

export {}; // needed to make TypeScript happy

declare global {
  interface Window {
    api: {
      send: (channel: string, data: any) => void;
      receive: (channel: string, func: () => void) => void;
      // etc.
    }
  }
}

@raphael10-collab
Copy link

raphael10-collab commented Feb 9, 2021

@TranquilMarmot

I created two files:

window.d.ts : 

export {};

declare global {
  interface Window {
    api: {

      send: (channel, data) => {
        ipcRenderer.invoke(channel, data).catch(e => console.log(e))
      },
      receive: (channel, func) => {
        console.log("preload-receive called. args: ");
        ipcRenderer.on(channel, (event, ...args) => func(...args));
      },
      electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
        ipcRenderer.sendTo(window_id, channel, arg);
      },
      electronIpcSend: (channel: string, ...arg: any) => {
        ipcRenderer.send(channel, arg);
      },
      electronIpcSendSync: (channel: string, ...arg: any) => {
        return ipcRenderer.sendSync(channel, arg);
      },
      electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.on(channel, listener);
      },
      electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.once(channel, listener);
      },
      electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) => void) => {
        ipcRenderer.removeListener(channel, listener);
      },
      electronIpcRemoveAllListeners: (channel: string) => {
        ipcRenderer.removeAllListeners(channel);
      }
    }
  }
}

window.ts :

window.api = {
  send: (channel, data) => {
    ipcRenderer.invoke(channel, data).catch(e => console.log(e))
  },
  receive: (channel, func) => {
    console.log("preload-receive called. args: ");
    ipcRenderer.on(channel, (event, ...args) => func(...args));
  },
  electronIpcSendTo: (window_id: string, channel: string, ...arg: any) => {
    ipcRenderer.sendTo(window_id, channel, arg);
  },
  electronIpcSend: (channel: string, ...arg: any) => {
    ipcRenderer.send(channel, arg);
  },
  electronIpcSendSync: (channel: string, ...arg: any) => {
    return ipcRenderer.sendSync(channel, arg);
  },
  electronIpcOn: (channel: string, listener: (event: any, ...arg: any) => void) => {
    ipcRenderer.on(channel, listener);
  },
  electronIpcOnce: (channel: string, listener: (event: any, ...arg: any) => void) => {
    ipcRenderer.once(channel, listener);
  },
  electronIpcRemoveListener:  (channel: string, listener: (event: any, ...arg: any) => void) => {
    ipcRenderer.removeListener(channel, listener);
  },
  electronIpcRemoveAllListeners: (channel: string) => {
    ipcRenderer.removeAllListeners(channel);
  }
}

And imported window.ts in main.ts and in src/components/App.tsx because of Electron apps added complexity of having two processes: main process and the renderer process.

The number of errors is now half: from 15 to 8.
But still have this kind of errors:

src/app/components/App.tsx:93:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

93       window.api.send('open-type-A-window', '');
                ~~~
src/app/components/App.tsx:95:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

95       window.api.electronIpcOn('window-A-opened', (event, args) => {
                ~~~

src/app/components/App.tsx:99:18 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

99           window.api.electronIpcSend('window-A-channel', filePath_1);
                    ~~~


src/app/components/App.tsx:103:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

103       window.api.electronIpcOn('window-A-channel', (event, args) => {
                 ~~~

src/app/components/FileList.tsx:9:83 - error TS2339: Property 'path' does not exist on type 'File'.

9     `'${file.name}' of size '${file.size}' , type '${file.type}' and path '${file.path}'`
                                                                                ~~~~

src/app/index.tsx:9:53 - error TS2339: Property 'hot' does not exist on type 'NodeModule'.

9 if (process.env.NODE_ENV == 'development' && module.hot) {
                                                   ~~~

src/app/index.tsx:10:10 - error TS2339: Property 'hot' does not exist on type 'NodeModule'.

10   module.hot.accept();
            ~~~

src/window.ts:1:8 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

1 window.api = {
         ~~~

Found 8 errors.

I've put all the code in github repo, easier to look at : https://github.com/raphael10-collab/Libp2pPlaying

@TranquilMarmot
Copy link

@raphael10-collab I opened a PR against your repo, I hope it is helpful!

@raphael10-collab
Copy link

Thank you very much @TranquilMarmot

There were and, unfortunately, there are a lot of errors.

I still have the same "Property 'api' does not exist on type 'Window & typeof globalThis'.
There must be a root cause to fix

src/app/components/App.tsx:91:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

91       window.api.send('open-type-A-window', '');
                ~~~

src/app/components/App.tsx:93:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

93       window.api.electronIpcOn('window-A-opened', (event, args) => {
                ~~~

src/app/components/App.tsx:97:18 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

97           window.api.electronIpcSend('window-A-channel', filePath_1);
                    ~~~

src/app/components/App.tsx:101:14 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

101       window.api.electronIpcOn('window-A-channel', (event, args) => {
                 ~~~

src/preload.ts:22:28 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

22         ipcRenderer.sendTo(window_id, channel, arg);
                              ~~~~~~~~~

src/window.ts:32:8 - error TS2339: Property 'api' does not exist on type 'Window & typeof globalThis'.

32 window.api = {
          ~~~

src/window.ts:34:5 - error TS2304: Cannot find name 'ipcRenderer'.

34     ipcRenderer.invoke(channel, data).catch((e) => console.log(e));
       ~~~~~~~~~~~

src/window.ts:38:5 - error TS2304: Cannot find name 'ipcRenderer'.

38     ipcRenderer.on(channel, (event, ...args) => func(event, ...args));
       ~~~~~~~~~~~

src/window.ts:41:5 - error TS2304: Cannot find name 'ipcRenderer'.

41     ipcRenderer.sendTo(window_id, channel, arg);
       ~~~~~~~~~~~

src/window.ts:44:5 - error TS2304: Cannot find name 'ipcRenderer'.

44     ipcRenderer.send(channel, arg);
       ~~~~~~~~~~~

src/window.ts:47:12 - error TS2304: Cannot find name 'ipcRenderer'.

47     return ipcRenderer.sendSync(channel, arg);
              ~~~~~~~~~~~

src/window.ts:53:5 - error TS2304: Cannot find name 'ipcRenderer'.

53     ipcRenderer.on(channel, listener);
       ~~~~~~~~~~~

src/window.ts:59:5 - error TS2304: Cannot find name 'ipcRenderer'.

59     ipcRenderer.once(channel, listener);
   ~~~~~~~~~~~

src/window.ts:65:5 - error TS2304: Cannot find name 'ipcRenderer'.

65     ipcRenderer.removeListener(channel, listener);
       ~~~~~~~~~~~

src/window.ts:68:5 - error TS2304: Cannot find name 'ipcRenderer'.

68 ipcRenderer.removeAllListeners(channel);
~~~~~~~~~~~

Found 15 errors.

@TranquilMarmot
Copy link

It works for me using the code I have in my PR against your branch. You might have better luck asking about something like this on Stack overflow! Good luck!

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

Successfully merging a pull request may close this issue.