TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Assigning window.location after 5.8.2 upgrade fails

Open AndreasPresthammer opened this issue 9 months ago • 4 comments

🔎 Search Terms

Unable to assign window.location typescript 5.8.2 error TS2322: Type 'Location' is not assignable to type 'string & Location'. Type 'Location' is not assignable to type 'string'.

🕗 Version & Regression Information

  • This changed between versions 5.7.3 and 5.8.2

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.2#code/O4SwdgJg9sB0A2UDGBDALiKYAEBebokMCy6mYA3AFBA

💻 Code

window.location = window.location;

🙁 Actual behavior

Failing with error:

error TS2322: Type 'Location' is not assignable to type 'string & Location'.
  Type 'Location' is not assignable to type 'string'.

🙂 Expected behavior

Expect it to either be a documented breaking change, OR it should continue to work as it did in 5.7.3:

https://www.typescriptlang.org/play/?ts=5.7.3#code/O4SwdgJg9sB0A2UDGBDALiKYAEBebokMCy6mYA3AFBA

Additional information about the issue

No response

AndreasPresthammer avatar Mar 03 '25 08:03 AndreasPresthammer

Related: #48949

MartinJohns avatar Mar 04 '25 00:03 MartinJohns

Had the same issue when upgrading to the new version.

A similar workaround to this one worked for me:

const win: typeof globalThis = window;
win.location = window.location; // Pass

I see the PR https://github.com/microsoft/TypeScript/pull/60987 added this part:

Document.location and Window.location now has stricter get/set types:

  • get location(): Location is unchanged
  • set location(href: string) previously allowed Location (probably because of Typescript's accessor assignability rules)

Seems to be the cause of the change, as set location expects a string, but globalThis['location'] expects a Location

chrisvltn avatar Mar 04 '25 09:03 chrisvltn

A pain for old style iframe prevention - ts-ignore helps

if (window.top !== window.self) {
  if (window.top) {  
    // @ts-ignore
    window.top.location = window.self.location;
  }
}

reifi avatar Mar 05 '25 09:03 reifi

How can anything satisfy the type string & Location? Was it supposed to be string | Location instead?

onlywei avatar Mar 13 '25 02:03 onlywei

Independent of whether or not this is a bug/regression: Shouldn't window.location.href = otherWindow.location.href do pretty much what you'd expect in most if not all of the cases?

clemens avatar Mar 31 '25 07:03 clemens

This is hitting us in our test files where we will often override window.location to mock it in order to test client-side JavaScript behaviour like this:

window.location = { ...window.location, pathname: '/modals/main/change-to-delivery' };

philwolstenholme avatar Mar 31 '25 16:03 philwolstenholme

@jakebailey @RyanCavanaugh sorry for tagging but this is a blocker for some codebases for upgrading to more recent TS versions, unless you add hundreds of ts-ignores. Is a fix planned, should someone make a PR or any other plans (it's been a month and no updates)?

kurtextrem avatar Apr 08 '25 14:04 kurtextrem

https://developer.mozilla.org/en-US/docs/Web/API/Window/location says that reading the variable gives a Location object, but that you can write a string to the variable.

I can imagine that the type being string & Location is itself a problem (probably some known issue with getters/setters; you'll note that the DOM types do not actually have this type anywhere, so it must be created by the checker), it seems to me that window.location = window.location is not actually legal, no?

Is this only working at runtime due to coercion to string since Location.toString() gives a string? I can't see how window.location = { ...anything } should work, as that wouldn't even have a toString.

The change came from: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1838

@saschanaz

jakebailey avatar Apr 08 '25 16:04 jakebailey

Is this only working at runtime due to coercion to string since Location.toString() gives a string? I can't see how window.location = { ...anything } should work, as that wouldn't even have a toString.

Yes, as far as I can tell from MDN and the spec. Writes to window.location are effectively forwarded to window.location.href, which coerces to a string.

snarbles2 avatar Apr 08 '25 17:04 snarbles2

window.location = window.location is effectively window.location.href = window.location.toString(). Which works on browsers, but only with two magics under the hood:

  1. location.href: The window.location setter is overriden so that the value goes to window.location.href instead.
  2. location.toString(): Given that location.href expects string, any passed value will be implicitly stringified.

The first magic is what's supported by https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1838. For the second magic, I think in general implicit stringification is not supported, if you want to pass a string then you should explicitly do so.

And window.location = window.location is just weird, maybe do location.reload() instead? If you feel strongly about the current code, you can do window.location.href = window.location.href which includes no magic.

saschanaz avatar Apr 08 '25 20:04 saschanaz

But the other part of me says maybe we should be less strict and allow passing Location where URLs are expected, as done in https://github.com/microsoft/TypeScript-DOM-lib-generator/commit/2bffd44cf19e5772489f6300a253e9d0dd4d8685.

saschanaz avatar Apr 08 '25 20:04 saschanaz

I note that this code gives a TypeScript error, which doesn't seem like it can be intentional? window.location = window.location;

Shelagh-Lewins avatar Apr 28 '25 13:04 Shelagh-Lewins

If you're here because you're stubbing some window location properties - you can bypass the assignment type check:

For example with a jest stub:

const oldWindowLocation = window.location;

Object.defineProperty(window, 'location', {
  configurable: true,
  value: Object.defineProperties(
    {},
    {
      ...Object.getOwnPropertyDescriptors(oldWindowLocation),
      assign: {
        configurable: true,
        value: jest.fn(),
      },
  ),
});

// do stuff... and then
Object.defineProperty(window, 'location', {
  configurable: true,
  value: oldWindowLocation,
});

tomh-t avatar May 08 '25 16:05 tomh-t

@tomh-t where should that be placed? @benmvp posted a similar fix a few years ago, but I can't either yours or his working. Typescript vis VS Code shows a missing property assignment near the end of the block. I tried at the top of the describe block, as well as inside the beforeEach block.

Image

commadelimited avatar Jun 26 '25 21:06 commadelimited

@commadelimited - looks like a small syntax error you need to fix. I wouldn't trust a copy & paste in a github comment character for character - I have to edit it in here to avoid sharing irrelevant code and writing code in a comment is susceptible to syntax errors (missing curly)

But anyway - we're using this in a helper

export const withLocationMock = () => {
  const oldWindowLocation = window.location;

  beforeEach(() => {
    Object.defineProperty(window, 'location', {
      configurable: true,
      value: Object.defineProperties(
        {},
        {
          ...Object.getOwnPropertyDescriptors(oldWindowLocation),
          assign: {
            configurable: true,
            value: jest.fn(),
          },
          reload: {
            configurable: true,
            value: jest.fn(),
          },
          host: {
            configurable: true,
            writable: true,
            value: 'initial-host-for-tests',
          },
          protocol: {
            configurable: true,
            writable: true,
            value: 'http:',
          },
        },
      ),
    });
  });

  afterEach(() => {
    Object.defineProperty(window, 'location', {
      configurable: true,
      value: oldWindowLocation,
    });
  });
};

And then in any test

describe('some test', () => {
  withLocationMock();

tomh-t avatar Jun 30 '25 08:06 tomh-t

Thanks my friend, I'll give that a shot...appreciated.

commadelimited avatar Jun 30 '25 18:06 commadelimited