TypeScript Version: 3.6.2
Search Terms:
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:
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";
Most helpful comment
Augmentation of
window
is usually done by using interface merging:or
For non-module scenarios.