Typescript: Unable to extend window in TS 3.6

Created on 29 Aug 2019  路  11Comments  路  Source: microsoft/TypeScript


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:

Working as Intended

Most helpful comment

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.

All 11 comments

Perhaps unrelated, but I ran across this spectacularly weird error while trying to work around this:

type T = Window & typeof globalThis;
interface MyWindow extends T {
  foo: string;
}

Error:

interface MyWindow
Property 'Infinity' of type 'number' is not assignable to numeric index type 'Window'.ts(2412)
Property 'NaN' of type 'number' is not assignable to numeric index type 'Window'.ts(2412)

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.

The appeal of extending Window and using an assertion is that it's not global. Though I suppose if you're extending a global singleton, you don't have much ground to stand on!

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.

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.

Thanks @sandersn. The missing piece was that window[0] is a way of accessing a frame on the page.

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.

Wait, what.

The index signature [n: number]: Window doesn't match "NaN": number or "Infinity": number at all (it's not a string index signature) so their types shouldn't matter for that purpose.

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

Oh I see, I didn鈥檛 realize those were covered by a numeric index signature because they鈥檙e not valid array indices. I know they are covered by number but for some reason I thought the signature was special-cased to only cover valid array indices (i.e. roughly the domain of Number.isSafeInteger).

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

quick and dirty 馃挜

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MartynasZilinskas picture MartynasZilinskas  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

remojansen picture remojansen  路  3Comments

manekinekko picture manekinekko  路  3Comments

blendsdk picture blendsdk  路  3Comments