Typescript: Expose global declarations on `window`

Created on 7 Nov 2017  Â·  13Comments  Â·  Source: microsoft/TypeScript

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:

  • #8866 window.Blob
  • #14402 window.createImageBitmap
  • #9325 window.Math
  • #3753 window.URL
  • #11305 window.MutationObserver
  • #18756 window.PointerEvent
  • #19758 window.MouseEvent
  • #23886 window.Element
  • #25651 window.PerformanceObserver

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 #14052), and allow Window to extend from it.
  2. generate lib.d.ts with all global declarations mirrored on Window.
In Discussion Suggestion

Most helpful comment

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 }
}

?

All 13 comments

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 https://github.com/Microsoft/TypeScript/issues/10050#issuecomment-236596782. 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.

window.Worker, same story...

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
}

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 https://github.com/Microsoft/TypeScript/pull/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?

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

What is the status on this?

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 }
}

?

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

@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.

@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

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

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!
  }
}

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
  }
}
Was this page helpful?
0 / 5 - 0 ratings