From 27053fe74bc396c55ad4e784454bd5356a03531e Mon Sep 17 00:00:00 2001 From: Mohak Gupta Date: Tue, 15 Sep 2026 06:52:57 +0530 Subject: [PATCH 1/2] lib: ZonedDateTime.toLocaleString must not accept a timeZone option The engines reject a timeZone option here because a ZonedDateTime already carries its time zone, and MDN documents that it must not be provided. lib.esnext.temporal typed the options as Intl.DateTimeFormatOptions, so the call type checked. Introduce ZonedDateTimeToLocaleStringOptions, which omits timeZone, and use it for ZonedDateTime.toLocaleString. The existing temporal.ts fixture already marked such a call as WRONG; its baseline now reports it. Fixes #64227 --- .../bundled/libs/lib.esnext.temporal.d.ts | 4 ++- .../reference/compiler/temporal.errors.txt | 5 +++- .../reference/compiler/temporal.types | 28 +++++++++---------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts b/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts index 2d18011132a25..6b056816c5277 100644 --- a/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts +++ b/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts @@ -272,6 +272,8 @@ declare namespace Temporal { timeZoneName?: "auto" | "never" | "critical" | undefined; } + interface ZonedDateTimeToLocaleStringOptions extends Omit {} + interface ZonedDateTimeFromOptions extends OverflowOptions, DisambiguationOptions { offset?: "use" | "ignore" | "prefer" | "reject" | undefined; } @@ -317,7 +319,7 @@ declare namespace Temporal { round(roundTo: RoundingOptions<"day" | TimeUnit>): ZonedDateTime; equals(other: ZonedDateTimeLike): boolean; toString(options?: ZonedDateTimeToStringOptions): string; - toLocaleString(locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions): string; + toLocaleString(locales?: Intl.LocalesArgument, options?: ZonedDateTimeToLocaleStringOptions): string; toJSON(): string; valueOf(): never; startOfDay(): ZonedDateTime; diff --git a/tsc/testdata/baselines/reference/compiler/temporal.errors.txt b/tsc/testdata/baselines/reference/compiler/temporal.errors.txt index 59d4e125441bd..016d75cd210f4 100644 --- a/tsc/testdata/baselines/reference/compiler/temporal.errors.txt +++ b/tsc/testdata/baselines/reference/compiler/temporal.errors.txt @@ -1,10 +1,11 @@ temporal.ts(25,13): error TS2339: Property 'year' does not exist on type 'Instant'. +temporal.ts(594,47): error TS2353: Object literal may only specify known properties, and 'timeZone' does not exist in type 'Omit'. temporal.ts(1504,8): error TS2339: Property 'month' does not exist on type 'PlainMonthDay'. temporal.ts(1514,8): error TS2339: Property 'month' does not exist on type 'PlainMonthDay'. temporal.ts(1520,8): error TS2339: Property 'month' does not exist on type 'PlainMonthDay'. -==== temporal.ts (4 errors) ==== +==== temporal.ts (5 errors) ==== /** * Test cases derived from documentation at tc39/proposal-temporal, * under the following license: @@ -601,6 +602,8 @@ temporal.ts(1520,8): error TS2339: Property 'month' does not exist on type 'Plai const options = { weekday: "long", year: "numeric", month: "long", day: "numeric" } as const; zdt.toLocaleString("de-DE", options); // => 'Sonntag, 1. Dezember 2019' /* WRONG */ zdt.toLocaleString("de-DE", { timeZone: "Pacific/Auckland" }); + ~~~~~~~~ +!!! error TS2353: Object literal may only specify known properties, and 'timeZone' does not exist in type 'Omit'. // => RangeError: Time zone option Pacific/Auckland does not match actual time zone Europe/Berlin zdt.withTimeZone("Pacific/Auckland").toLocaleString("de-DE"); // => '2.12.2019, 0:00:00 GMT+13' zdt.toLocaleString("en-US-u-nu-fullwide-hc-h12"); // => '12/1/2019, 12:00:00 PM GMT+1' diff --git a/tsc/testdata/baselines/reference/compiler/temporal.types b/tsc/testdata/baselines/reference/compiler/temporal.types index 39088d06c4f34..e64568948eb45 100644 --- a/tsc/testdata/baselines/reference/compiler/temporal.types +++ b/tsc/testdata/baselines/reference/compiler/temporal.types @@ -1598,9 +1598,9 @@ >zdt => zdt.toLocaleString("en", { month: "long" }) : (zdt: Temporal.ZonedDateTime) => string >zdt : Temporal.ZonedDateTime >zdt.toLocaleString("en", { month: "long" }) : string ->zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt : Temporal.ZonedDateTime ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >"en" : "en" >{ month: "long" } : { month: "long"; } >month : "long" @@ -2604,15 +2604,15 @@ zdt.toLocaleString(); // example output: 12/1/2019, 12:00:00 PM >zdt.toLocaleString() : string ->zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt : Temporal.ZonedDateTime ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string zdt.toLocaleString("de-DE"); // => '1.12.2019, 12:00:00 MEZ' >zdt.toLocaleString("de-DE") : string ->zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt : Temporal.ZonedDateTime ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >"de-DE" : "de-DE" const options = { weekday: "long", year: "numeric", month: "long", day: "numeric" } as const; @@ -2630,17 +2630,17 @@ zdt.toLocaleString("de-DE", options); // => 'Sonntag, 1. Dezember 2019' >zdt.toLocaleString("de-DE", options) : string ->zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt : Temporal.ZonedDateTime ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >"de-DE" : "de-DE" >options : { readonly weekday: "long"; readonly year: "numeric"; readonly month: "long"; readonly day: "numeric"; } /* WRONG */ zdt.toLocaleString("de-DE", { timeZone: "Pacific/Auckland" }); >zdt.toLocaleString("de-DE", { timeZone: "Pacific/Auckland" }) : string ->zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt : Temporal.ZonedDateTime ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >"de-DE" : "de-DE" >{ timeZone: "Pacific/Auckland" } : { timeZone: string; } >timeZone : string @@ -2649,20 +2649,20 @@ // => RangeError: Time zone option Pacific/Auckland does not match actual time zone Europe/Berlin zdt.withTimeZone("Pacific/Auckland").toLocaleString("de-DE"); // => '2.12.2019, 0:00:00 GMT+13' >zdt.withTimeZone("Pacific/Auckland").toLocaleString("de-DE") : string ->zdt.withTimeZone("Pacific/Auckland").toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.withTimeZone("Pacific/Auckland").toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt.withTimeZone("Pacific/Auckland") : Temporal.ZonedDateTime >zdt.withTimeZone : (timeZone: Temporal.TimeZoneLike) => Temporal.ZonedDateTime >zdt : Temporal.ZonedDateTime >withTimeZone : (timeZone: Temporal.TimeZoneLike) => Temporal.ZonedDateTime >"Pacific/Auckland" : "Pacific/Auckland" ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >"de-DE" : "de-DE" zdt.toLocaleString("en-US-u-nu-fullwide-hc-h12"); // => '12/1/2019, 12:00:00 PM GMT+1' >zdt.toLocaleString("en-US-u-nu-fullwide-hc-h12") : string ->zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >zdt : Temporal.ZonedDateTime ->toLocaleString : (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string >"en-US-u-nu-fullwide-hc-h12" : "en-US-u-nu-fullwide-hc-h12" } From d1c21a18ac7beb6dd348a8e24309be8ed258d9ef Mon Sep 17 00:00:00 2001 From: Mohak Gupta Date: Mon, 21 Sep 2026 10:37:07 +0530 Subject: [PATCH 2/2] lib: close the widened variable hole in the timeZone omission Omit only applies excess property checking to an object literal, so a variable typed Intl.DateTimeFormatOptions still carried a timeZone into ZonedDateTime.toLocaleString. Declaring the property as never refuses it in both forms, because string is not assignable to never. The added fixture covers the variable form, which is the one the omission did not catch. --- .../bundled/libs/lib.esnext.temporal.d.ts | 4 ++- .../reference/compiler/temporal.errors.txt | 22 +++++++++++++-- .../baselines/reference/compiler/temporal.js | 13 +++++++++ .../reference/compiler/temporal.symbols | 23 +++++++++++++++ .../reference/compiler/temporal.types | 28 +++++++++++++++++++ tsc/testdata/tests/cases/compiler/temporal.ts | 7 +++++ 6 files changed, 93 insertions(+), 4 deletions(-) diff --git a/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts b/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts index 6b056816c5277..b27f5bf30c5c7 100644 --- a/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts +++ b/tsc/internal/bundled/libs/lib.esnext.temporal.d.ts @@ -272,7 +272,9 @@ declare namespace Temporal { timeZoneName?: "auto" | "never" | "critical" | undefined; } - interface ZonedDateTimeToLocaleStringOptions extends Omit {} + interface ZonedDateTimeToLocaleStringOptions extends Intl.DateTimeFormatOptions { + timeZone?: never; + } interface ZonedDateTimeFromOptions extends OverflowOptions, DisambiguationOptions { offset?: "use" | "ignore" | "prefer" | "reject" | undefined; diff --git a/tsc/testdata/baselines/reference/compiler/temporal.errors.txt b/tsc/testdata/baselines/reference/compiler/temporal.errors.txt index 016d75cd210f4..df73f0ee79e07 100644 --- a/tsc/testdata/baselines/reference/compiler/temporal.errors.txt +++ b/tsc/testdata/baselines/reference/compiler/temporal.errors.txt @@ -1,11 +1,15 @@ temporal.ts(25,13): error TS2339: Property 'year' does not exist on type 'Instant'. -temporal.ts(594,47): error TS2353: Object literal may only specify known properties, and 'timeZone' does not exist in type 'Omit'. +temporal.ts(594,47): error TS2322: Type 'string' is not assignable to type 'undefined'. temporal.ts(1504,8): error TS2339: Property 'month' does not exist on type 'PlainMonthDay'. temporal.ts(1514,8): error TS2339: Property 'month' does not exist on type 'PlainMonthDay'. temporal.ts(1520,8): error TS2339: Property 'month' does not exist on type 'PlainMonthDay'. +temporal.ts(1826,45): error TS2345: Argument of type 'DateTimeFormatOptions' is not assignable to parameter of type 'ZonedDateTimeToLocaleStringOptions'. + Types of property 'timeZone' are incompatible. + Type 'string | undefined' is not assignable to type 'undefined'. + Type 'string' is not assignable to type 'undefined'. -==== temporal.ts (5 errors) ==== +==== temporal.ts (6 errors) ==== /** * Test cases derived from documentation at tc39/proposal-temporal, * under the following license: @@ -603,7 +607,7 @@ temporal.ts(1520,8): error TS2339: Property 'month' does not exist on type 'Plai zdt.toLocaleString("de-DE", options); // => 'Sonntag, 1. Dezember 2019' /* WRONG */ zdt.toLocaleString("de-DE", { timeZone: "Pacific/Auckland" }); ~~~~~~~~ -!!! error TS2353: Object literal may only specify known properties, and 'timeZone' does not exist in type 'Omit'. +!!! error TS2322: Type 'string' is not assignable to type 'undefined'. // => RangeError: Time zone option Pacific/Auckland does not match actual time zone Europe/Berlin zdt.withTimeZone("Pacific/Auckland").toLocaleString("de-DE"); // => '2.12.2019, 0:00:00 GMT+13' zdt.toLocaleString("en-US-u-nu-fullwide-hc-h12"); // => '12/1/2019, 12:00:00 PM GMT+1' @@ -1836,4 +1840,16 @@ temporal.ts(1520,8): error TS2339: Property 'month' does not exist on type 'Plai Temporal.Now.plainTimeISO(); // get the current wall-clock time in the system time zone and ISO-8601 calendar Temporal.Now.plainDateTimeISO(); // same as above, but return the DateTime in the ISO-8601 calendar } + + { + // Refused through a widened variable too, not only in an object literal. + const zdt = Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]"); + const widened: Intl.DateTimeFormatOptions = { timeZone: "Pacific/Auckland" }; + /* WRONG */ zdt.toLocaleString("de-DE", widened); + ~~~~~~~ +!!! error TS2345: Argument of type 'DateTimeFormatOptions' is not assignable to parameter of type 'ZonedDateTimeToLocaleStringOptions'. +!!! error TS2345: Types of property 'timeZone' are incompatible. +!!! error TS2345: Type 'string | undefined' is not assignable to type 'undefined'. +!!! error TS2345: Type 'string' is not assignable to type 'undefined'. + } \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/temporal.js b/tsc/testdata/baselines/reference/compiler/temporal.js index 951ddf52662ec..7086a2fbfa169 100644 --- a/tsc/testdata/baselines/reference/compiler/temporal.js +++ b/tsc/testdata/baselines/reference/compiler/temporal.js @@ -1821,6 +1821,13 @@ Temporal.Now.plainTimeISO(); // get the current wall-clock time in the system time zone and ISO-8601 calendar Temporal.Now.plainDateTimeISO(); // same as above, but return the DateTime in the ISO-8601 calendar } + +{ + // Refused through a widened variable too, not only in an object literal. + const zdt = Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]"); + const widened: Intl.DateTimeFormatOptions = { timeZone: "Pacific/Auckland" }; + /* WRONG */ zdt.toLocaleString("de-DE", widened); +} //// [temporal.js] @@ -3371,3 +3378,9 @@ Temporal.Now.plainTimeISO(); // get the current wall-clock time in the system time zone and ISO-8601 calendar Temporal.Now.plainDateTimeISO(); // same as above, but return the DateTime in the ISO-8601 calendar } +{ + // Refused through a widened variable too, not only in an object literal. + const zdt = Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]"); + const widened = { timeZone: "Pacific/Auckland" }; + /* WRONG */ zdt.toLocaleString("de-DE", widened); +} diff --git a/tsc/testdata/baselines/reference/compiler/temporal.symbols b/tsc/testdata/baselines/reference/compiler/temporal.symbols index f232e7509e447..afed1f0d5b854 100644 --- a/tsc/testdata/baselines/reference/compiler/temporal.symbols +++ b/tsc/testdata/baselines/reference/compiler/temporal.symbols @@ -7029,3 +7029,26 @@ >plainDateTimeISO : Symbol(Temporal.Now.plainDateTimeISO, Decl(lib.esnext.temporal.d.ts, --, --)) } +{ + // Refused through a widened variable too, not only in an object literal. + const zdt = Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]"); +>zdt : Symbol(zdt, Decl(temporal.ts, 1823, 9)) +>Temporal.ZonedDateTime.from : Symbol(Temporal.ZonedDateTimeConstructor.from, Decl(lib.esnext.temporal.d.ts, --, --)) +>Temporal.ZonedDateTime : Symbol(Temporal.ZonedDateTime, Decl(lib.esnext.temporal.d.ts, --, --), Decl(lib.esnext.temporal.d.ts, --, --)) +>Temporal : Symbol(Temporal, Decl(lib.esnext.temporal.d.ts, --, --)) +>ZonedDateTime : Symbol(Temporal.ZonedDateTime, Decl(lib.esnext.temporal.d.ts, --, --), Decl(lib.esnext.temporal.d.ts, --, --)) +>from : Symbol(Temporal.ZonedDateTimeConstructor.from, Decl(lib.esnext.temporal.d.ts, --, --)) + + const widened: Intl.DateTimeFormatOptions = { timeZone: "Pacific/Auckland" }; +>widened : Symbol(widened, Decl(temporal.ts, 1824, 9)) +>Intl : Symbol(Intl, Decl(lib.es5.d.ts, --, --), Decl(lib.es2018.intl.d.ts, --, --), Decl(lib.es2020.intl.d.ts, --, --), Decl(lib.es2021.intl.d.ts, --, --), Decl(lib.es2025.intl.d.ts, --, --) ... and 1 more) +>DateTimeFormatOptions : Symbol(Intl.DateTimeFormatOptions, Decl(lib.es5.d.ts, --, --), Decl(lib.es2020.intl.d.ts, --, --), Decl(lib.es2021.intl.d.ts, --, --)) +>timeZone : Symbol(timeZone, Decl(temporal.ts, 1824, 49)) + + /* WRONG */ zdt.toLocaleString("de-DE", widened); +>zdt.toLocaleString : Symbol(Temporal.ZonedDateTime.toLocaleString, Decl(lib.esnext.temporal.d.ts, --, --)) +>zdt : Symbol(zdt, Decl(temporal.ts, 1823, 9)) +>toLocaleString : Symbol(Temporal.ZonedDateTime.toLocaleString, Decl(lib.esnext.temporal.d.ts, --, --)) +>widened : Symbol(widened, Decl(temporal.ts, 1824, 9)) +} + diff --git a/tsc/testdata/baselines/reference/compiler/temporal.types b/tsc/testdata/baselines/reference/compiler/temporal.types index e64568948eb45..6533709ebba9e 100644 --- a/tsc/testdata/baselines/reference/compiler/temporal.types +++ b/tsc/testdata/baselines/reference/compiler/temporal.types @@ -9126,3 +9126,31 @@ >plainDateTimeISO : (timeZone?: Temporal.TimeZoneLike) => Temporal.PlainDateTime } +{ + // Refused through a widened variable too, not only in an object literal. + const zdt = Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]"); +>zdt : Temporal.ZonedDateTime +>Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]") : Temporal.ZonedDateTime +>Temporal.ZonedDateTime.from : (item: Temporal.ZonedDateTimeLike, options?: Temporal.ZonedDateTimeFromOptions) => Temporal.ZonedDateTime +>Temporal.ZonedDateTime : Temporal.ZonedDateTimeConstructor +>Temporal : typeof Temporal +>ZonedDateTime : Temporal.ZonedDateTimeConstructor +>from : (item: Temporal.ZonedDateTimeLike, options?: Temporal.ZonedDateTimeFromOptions) => Temporal.ZonedDateTime +>"2019-12-01T12:00+01:00[Europe/Berlin]" : "2019-12-01T12:00+01:00[Europe/Berlin]" + + const widened: Intl.DateTimeFormatOptions = { timeZone: "Pacific/Auckland" }; +>widened : Intl.DateTimeFormatOptions +>Intl : any +>{ timeZone: "Pacific/Auckland" } : { timeZone: string; } +>timeZone : string +>"Pacific/Auckland" : "Pacific/Auckland" + + /* WRONG */ zdt.toLocaleString("de-DE", widened); +>zdt.toLocaleString("de-DE", widened) : string +>zdt.toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string +>zdt : Temporal.ZonedDateTime +>toLocaleString : (locales?: Intl.LocalesArgument, options?: Temporal.ZonedDateTimeToLocaleStringOptions) => string +>"de-DE" : "de-DE" +>widened : Intl.DateTimeFormatOptions +} + diff --git a/tsc/testdata/tests/cases/compiler/temporal.ts b/tsc/testdata/tests/cases/compiler/temporal.ts index e4c4d9472fd2b..d5c0af1887782 100644 --- a/tsc/testdata/tests/cases/compiler/temporal.ts +++ b/tsc/testdata/tests/cases/compiler/temporal.ts @@ -1822,3 +1822,10 @@ Temporal.Now.plainTimeISO(); // get the current wall-clock time in the system time zone and ISO-8601 calendar Temporal.Now.plainDateTimeISO(); // same as above, but return the DateTime in the ISO-8601 calendar } + +{ + // Refused through a widened variable too, not only in an object literal. + const zdt = Temporal.ZonedDateTime.from("2019-12-01T12:00+01:00[Europe/Berlin]"); + const widened: Intl.DateTimeFormatOptions = { timeZone: "Pacific/Auckland" }; + /* WRONG */ zdt.toLocaleString("de-DE", widened); +}