[typescript-fetch] centralise date handling, add dateLibrary, fix for…
Fixes #24636
Short version:
typescript-fetch writes its date conversion out by hand in four templates plus querystring, and the copies have drifted.
format: date ends up unconverted for form parameters, and everywhere else it goes through UTC, which moves the day for any code using the local half of the Date API.
The issue has the measurements.
Design:
- (de)serialize
dateas a local date, keep UTC fordate-time.- This fixes the bug where parsing / serializing in UTC shifts the day. It shifts in
every timezone except UTC, in one direction or the other. - A full-date has no offset, so both ends have to agree on a wall clock. Local is the
one the surrounding code already uses:new Date(Y, M, D),getDate()and
toLocaleDateString()are all local. It also lines up with the other generators,
whereformat: datemaps toLocalDate,NaiveDateordatetime.dateand the day
you read back is the day the server sent. - create helpers in
runtime.tsso that every path uses the same
serialization/deserialization
- This fixes the bug where parsing / serializing in UTC shifts the day. It shifts in
- add the missing
isDateTypebranch for form parameters, the only location that was
converting nothing at all. - add a generation parameter
dateLibraryto choose howdateanddate-timeare
represented.- Same pattern as what is done in java, kotlin and dart.
- two options:
datefor a nativeDateconverted by the runtime, andstringas an
explicit escape hatch, handing back exactly what the server sent for consumers who
want to own date handling (SSR/RSC, or an app that already has a date library) withoutRuntimeChecksstill impliesstring. Maybe you'd like it to be explicit?- only generate the
datehelpers if we usedateLibrary: date.serializeDateTime
always ships, sincequerystringcan be handed aDateeither way
Temporal.PlainDate / Temporal.Instant would remove the wall-clock choice entirely and
is the proper fix here, but it can't be the default while Safari hasn't shipped it. I have
a follow-up drafted to add it as a third dateLibrary value. Would you rather have it in
this PR?
Behaviour change
Today a format: date is parsed to the instant of UTC midnight. This PR parses it to
local midnight instead, which is the position @macjohnny took in #17284 ("someone uses a
local date and expects the local representation to be transferred over the wire"). That
issue and #17272 ask the same question for the typescript generator and have sat since
December 2023. This PR only touches typescript-fetch.
Changing the anchor is not free. Reading the value as a plain date keeps working, or
starts working. Anything that relied on the instant being UTC midnight changes:
// '2026-08-05' received from the API, in Europe/Paris
// before: parsed to UTC midnight
date.getDate() // 5
date.getUTCDate() // 5
date.toISOString() // '2026-08-05T00:00:00.000Z'
// after: parsed to local midnight
date.getDate() // 5
date.getUTCDate() // 4
date.toISOString() // '2026-08-04T22:00:00.000Z'
So it breaks consumers who read dates with getUTC*, compare them against
Date.UTC(...), or persist and log the raw instant. Server-side rendering is affected too,
since the instant now depends on the process time zone.
dateLibrary: string is the way out, and it reproduces the current behaviour exactly:
new Date(value) and value.toISOString().substring(0, 10) are what the generator does
today.
Testing
- 5 new tests in
TypeScriptFetchClientCodegenTest, covering the default,
dateLibrary: string, thewithoutRuntimeChecksfallback, serialization per parameter
location, and the local-calendar semantics.TypeScriptFetchClientOptionsTestand its
provider pick up the new option. The whole typescript group passes: 192 tests. - New fixture
3_0/typescript-fetch/date-handling.yamlwith aformat: datein a model
property and in a path, query and form parameter, plus one sample build per option value
so the difference between them is a readable diff. tsc --strictover all 19typescript-fetchsample builds: 15 clean, 4 with the same
error counts as master (a123operationId, and missingimmutable/redux-ts-simple).- Checked the generated helpers in Pacific/Honolulu, America/Los_Angeles, America/New_York,
UTC, Europe/Paris, Asia/Tokyo and Pacific/Auckland. Round-trip, date-picker input and
display are correct in every one. Before, display was wrong west of UTC and the
date-picker path was wrong east of it.
PR checklist
- Read the contribution guidelines.
- Ran
./mvnw clean package,./bin/generate-samples.sh ./bin/configs/typescript-fetch*.yamland./bin/utils/export_generator.sh typescript-fetch, and committed all changed files. - @mention the TypeScript technical committee: @TiFu @taxpon @sebastianhaas @kenisteward @Vrolijkx @macjohnny @topce @akehir @petejohansonxo @amakhrov @davidgamero @mkusaka @joscha @KannaKim
Summary by cubic
Centralized date handling in typescript-fetch, added a dateLibrary option, and fixed format: date serialization across path/query/form params and querystrings. date now uses the local calendar; date-time stays UTC, with ES6-safe helpers that validate edge cases.
-
New Features
dateLibraryoption:date(default, nativeDate) orstring(pass-through).withoutRuntimeChecksimpliesstring.- Unified helpers:
parseDate,parseDateTime,serializeDate,serializeDateTimeused in models, oneOf guards, and all parameter locations. - Helper hardening: ES6-safe (no
padStart), preserves years 0000–0099, rejects invalid/DST-skipped dates, throws on invalidDate. Models without date fields no longer import helpers.
-
Migration
format: dateis now parsed/serialized as a local date, not UTC midnight.- If you depend on UTC-based or SSR-stable instants, set
dateLibrary: stringor update code that usesgetUTC*/toISOString()for plain dates.