TypeScript: Unable to extend window in TS 3.6

TypeScript Version: 3.6.2

Search Terms:

  • globalThis

Code

This code passed the type checker in TS 3.5:

interface MyWindow extends Window {
  foo: string;
}

(window as MyWindow).foo = 'bar';

but fails with this error in TS 3.6:

Conversion of type 'Window & typeof globalThis' to type 'MyWindow' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Property 'foo' is missing in type 'Window & typeof globalThis' but required in type 'MyWindow'.ts(2352)

Following the release notes, I can work around the issue by changing from interface to type and using an intersection:

type MyWindow = (typeof window) & {
  foo: string;
}

(window as MyWindow).foo = 'bar';

but this feels more obscure than the old way. Is this WAI? What’s the rationale for the non-extendable Window?

Expected behavior:

An interface should be able to extend Window.

Actual behavior:

The error above.

Playground Link: 3.6 isn’t on the playground yet.

Related Issues:

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 28
  • Comments: 18 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Augmentation of window is usually done by using interface merging:

export {}
declare global {
  interface Window {
    foo: string;
  }
}

window.foo = 'bar';

or

interface Window {
  foo: string;
}

window.foo = 'bar';

For non-module scenarios.

quick and dirty 💥

(window as any).foo = "bar";

@fatcerberus I don’t believe this statement is correct:

When using modules, the interface merge will be local to the file it occurs in, I believe. It’s true you can’t make the scope any narrower than that, but at least it won’t poison outside code.

I put this in a new file in my project (foo.ts) that’s not imported anywhere:

export {};
declare global {
  interface Document {
    /** documentation on foo */
    foo: string;
  }
}

Then, in another file, I can write:

document.foo;

without importing foo. There’s no error and I see the documentation. So I do not believe the augmentation is scoped to the module at all. It is as global as the declare global implies.

To force the file to be registered as a module, not a script

@orta Can I ask why it needs to be registered as a module?

For me this doesn’t work until the add the export {} - I’m just trying to understand why.

declare global {
  interface Window {
    myProp: string;
  }
}

Why not include globalThis at the point of the assertion?

(window as MyWindow & typeof globalThis).foo = 'bar';

This is indeed working as intended. window.foo = 'bar''s runtime behaviour is best represented by an interface merge, not a subtype. Can you actually even extend window? It seems like a convenient lie to the compiler.

As for the weird error, Window has a numeric index signature { [n: number]: Window } so that window[0] : Window. But when making sure an extends is legal, the compiler checks that numeric properties from the other side of the intersection have type Window. Normally, that’s properties with names like '0' and '1', but guess what NaN and Infinity are also numbers, so we check that their types are compatible with Window’s index signatures. Since NaN: number and number is not assignable to Window, the compiler reports that as an error.

@fatcerberus the property is named NaN, which is definitely a number. The funniest number.

To force the file to be registered as a module, not a script

The solution we recommend at Google is to make a local subtype for explicit casts, which avoids polluting the global type definitions. Because of the NaN/Infinity issue, we have a library with

type GlobalThis = typeof globalThis & Window & {
  NaN: never;
  Infinity: never;
};

which can then be used like so: (playground)

interface MyGlobal extends GlobalThis {
  readonly foo: number;
}
use((window as MyGlobal).foo);

If you want the type augmentation to be global and ambient, this works fine:

// somefile.d.ts
declare interface Window {
  foo: string
}

The trick is, you must put this in a file with the extension .d.ts (and there must not be a same-named file with just the extension .ts in the same directory. Then this will function as an ambient type and declaration merging will kick in. No import required.