Would have been interesting to connect back to Java's own journey to improve its time APIs, with Joda-Time leading into JSR 310, released with Java 8 in 2014. Immutable representations, instants, proper timezone support etc.
Given that the article refers to the "radical proposal" to bring these features to JavaScript came in 2018, surely Java's own solutions had some influence?
I'm in the C# world and can attribute most of my understanding about dates and times to Noda (the .NET version of Joda). Shout out to Jon Skeet for maintaining it.
Depending on the situation, the data lives either within the browser or within the OS. Chrome releases ship versions of tzdata that correspond to the version of tzdata shipped with the ICU it uses, and they do backport updates to prior Chrome releases within a certain window. Apple has a sideband way of deploying tzdata to all devices that doesn't appear via the normal Software Update mechanism. So it all depends on which particular OS/browser combo you're interested in and the decisions those owners made.
This is why I'm sticking with moment.js for now. I don't like that it's not immutable, but I value bundling timezone data into the app too much. Our customers are likely to use outdated browsers at their workplaces (we even had to maintain IE11 compatibility a bit too long for our liking).
A big step in the right direction, but I still don't like the API, here's why:
Especially in JavaScript where I often share a lot of code between the client and the server and therefore also transfer data between them, I like to strictly separate data from logic. What i mean by this is that all my data is plain JSON and no class instances or objects that have function properties, so that I can serialize/deserialize it easily.
This is not the case for Temporal objects. Also, the temporal objects have functions on them, which, granted, makes it convenient to use, but a pain to pass it over the wire.
I'd clearly prefer a set of pure functions, into which I can pass data-only temporal objects, quite a bit like date-fns did it.
Updating JSON.parse() to automatically create Temporal objects (from what shape of JSON value?) without a custom reviver would be a step too far, in my opinion.
This is effectively no different from Date:
serialize: date.toJSON()
deserialize: new Date(jsonDate)
Don’t JavaScript Date instances have the same problem? Date implements toJSON, but when parsing JSON you’ll have to manually identify which string values represent Dates and convert them back to Date instances. The exact same is true of Temporal (e.g. Instant).
And as far as I know, date-fns deals with native Date instances, not “data-only objects.”
The serialization thing is real but I don't think OOP vs functional is the actual issue here. JSON has no date type, period. You JSON.stringify a Date, get an ISO string, and hope whoever's parsing remembers to reconstruct it. Temporal doesn't fix that part, but at least when you do reconstruct you're saying "this is a ZonedDateTime" vs "this is an Instant" instead of everything being one ambiguous Date object.
> What i mean by this is that all my data is plain JSON and no class instances or objects that have function properties, so that I can serialize/deserialize it easily.
This is known as the "primitive obsession" anti-pattern.
I mean, I guess it's two steps forward and one step back ... but couldn't they have come up with something that was just two steps forward, and none back ... instead of making us write this nightmare all over the place?
Firstly, I really want this also and am supportive of an opinionated decision to put something at say Temporal.DateTime() that would be logical for developers to use ‘most of the time’.
However my guess is that the spec designers saw this lack of specivity as part of the problem.
A key issue of dates and times is that we use them culturally in day to day use in very imprecise ways and much is inferred from the context of use.
The concepts of zoned time and “wall clock” time are irreducable and it’s likely much code will be improved by forcing the developer to be explicit with the form of time they want to use and need for their particular use case.
I think this is why it’s so explicitly specified right now.
But I agree; I’ve often struggled with how verbose js can be.
Maybe with time (pun intended), more syntactic sugar and shorter conventions can be added to expand what has been an incredible effort to fix deep rooted issues.
I think that it's nice it's explicit that the method returns the current instant, rather than some other zero value.
There's also other methods that return other types, like
const now = Temporal.Now.instant()
which isn't as bad.
One could argue that the ugliness of the API intentionally reveals the ugliness of datetime. It forces you to really think about what you mean when you want "the current date time," which I think is one of the goals of the API.
I'd argue that `new Date()` returning the current time is a design mistake, and it should at least have been something like `DateTime.now()`. (Especially because it's called a date but it actually returns a timestamp: the footgun potential is large). C#'s date API isn't the best design (otherwise [NodaTime](https://www.nodatime.org/) wouldn't have been necessary) but it at least got some things right: you don't get the current time by doing `new DateTime()`, you get it by referencing `DateTime.UtcNow` for UTC (almost always what you want), or `DateTime.Now` for local time (which is sometimes what you want, but you should always stop and think about whether you really want UTC).
And even with C#'s date API, I've seen errors. For example, a library that formatted datetime strings by merely writing them out and adding a "Z" to the end, assuming that they would always be receiving UTC datetimes — and elsewhere in the code, someone passing `DateTime.Now` to that library. (I'm guessing the dev who wrote that was in the UK and wrote it during winter time, otherwise he would have noticed that the timestamps were coming out wrong. If he was in the US they'd be 4-7 or 5-8 hours wrong depending on whether DST was in effect. But in the UK during winter, local time equals UTC and you might not notice that mistake).
This is another reason why Temporal's API making clear distinctions between the different types, and requiring you to call conversion functions to switch between them, is a good idea. That C# mistake would have been harder (not impossible, people can always misunderstand an API, but harder) if the library had been using Nodatime. And Temporal is based on the same design principles (not identical APIs, just simmilar principles) as Nodatime.
I also find it super more complicated and messy than what you can find in another language without proper justification.
Like the Temporal.Instant with the only difference that is now but in nanosecond.
Would have been better to be Now with a suffix to indicate that it is more precise. Or even better, it is just the function you use or parameter that give the precision.
And why Now as the name space? I would expect the opposite, like python, you have something like
Temporal.Date, and from there you get a date of now or a specific time, with or without timezone info, ...
I'm one of the volunteer open-source folks (called "proposal champions" in TC39 parlance) who designed the Temporal API. Sorry for the late reply, as you can imagine it's been a busy week for us.
You raise a very important question: why is `Temporal.Now.zonedDateTimeISO()` so verbose? After 10 years of work on Temporal, you can assume that every API design and naming decision has been argued about, often many times. I'll try to summarize the reasoning and history below to explain how we got there.
There's really five questions here. There's no perfect order to answer them because the answers overlap somewhat, so you may want to read to the end until disagreeing. :-) I will have to split this response up into two posts because it's long.
1. Why do we need `Temporal.Now.`? Why not just `Temporal.`?
The TC39 members working on security required us to have a forward-compatible way to override and/or prevent access to information about the local machine. With the current design, a hosting environment can monkey-patch or remove `Temporal.Now`, and regardless of what Temporal changes in the future that local-machine info will still be blocked or overridden. But if we'd spread methods across each Temporal type (e.g. `Temporal.PlainDate.today()`, `Temporal.Instant.now()`) then a new Temporal type added in the future could allow jailbreaking to get local machine info.
This isn't just a security concern. Environments like jest may want to control the local time that tests see. Hosters of JS code like https://convex.dev/ might want to override the local time so that transactions can be re-run deterministically on another node. And so on.
2. Why a `Temporal.*` namespace? Why not just `ZonedDateTime`, `PlainTime`, etc. in the global namespace?
If `Date` didn't exist, we'd probably have done exactly this, and argued harder about (1) above, and we maybe could have won that argument!
But we were worried about the confusion between `Date` and another type named `PlainDate` (or whatever we could have called the date-only type). By putting all Temporal types in a namespace, it was much harder to be confused.
A secondary benefit of a namespace was to expose developers (via autocomplete in editors and browser dev tools) to the full set of types available, so that developers would pick the right type for their use case. But the `Date` conflict was the main reason.
We could have made the types TemporalDate, TemporalTime, etc. but that shaves only one character and violates (1) so there was no chance that would move forward. So we went with a namespace.
3. Why is it `Temporal.Now.zonedDateTimeISO()` not `Temporal.now()`?
As @rmunn and others discuss below, one of the core tenets of Temporal (following in the footsteps of Java, Noda time, Rust, and many others) was that developers must figure out up-front what kind of date/time data they want, and then pick the appropriate Temporal type for that data.
For a canonical example of why this is needed, just think of the thousands (millions?) of times that JS developers or end users have ended up with off-by-one-day errors because `new Date()` reports a different date depending on which time zone you're in, even if all you care about is displaying a date where the time zone is not relevant.
It's a reasonable argument that because `Temporal.ZonedDateTime` represents the superset of all Temporal types, it should occupy a privileged position and should be the default Temporal type for something like `Temporal.now()`. Given that I proposed the initial design of the ZonedDateTime type (https://github.com/tc39/proposal-temporal/pull/700) I'm understandably sympathetic to that point of view!
But there are performance concerns. Accessing the system time zone and calenda...
My playbook for JavaScript dates is.. store in UTC.. exchange only in UTC.. convert to locale date time only in the presentation logic. This has worked well for me enough that Im skeptical of needing anything else
That generally works for timestamps (Temporal Instant). But it doesn’t work for representing calendar dates with no time information (Temporal PlainDate) unless you add an additional strict convention like “calendar dates are always represented as midnight UTC”).
"Just use UTC" is another, albeit more subtle, falsehood programmers believe about date/time.
It's fine for distributed logging and computer-only usage, but fails in obscure ways once humans, time zones, travel, laws, and/or daylight saving time get involved.
If you're scheduling events for humans, and can't immediately list the reasons your app is an exception to the above, store the time zone to be safe. You probably don't have big data, and nobody will notice the minuscule overhead.
An example that is hard to follow defeats the point. It's just showing what pattern is possible and you can imagine the abstraction layers and indirection that would make it happen accidentally.
86 comments
[ 2.7 ms ] story [ 78.4 ms ] threadCongrats to all the champions who worked super hard on this for so long! It's been fun working on temporal_rs for the last couple years :)
Given that the article refers to the "radical proposal" to bring these features to JavaScript came in 2018, surely Java's own solutions had some influence?
Safari confirmed as IE Spiritual successor in 2020+.
Date is out, Temporal is in
https://news.ycombinator.com/item?id=46589658
I didn't spot how Temporal fixes this. What happens when "now" changes? Does the library get updated and pushed out rapidly via browsers?
This is not the case for Temporal objects. Also, the temporal objects have functions on them, which, granted, makes it convenient to use, but a pain to pass it over the wire.
I'd clearly prefer a set of pure functions, into which I can pass data-only temporal objects, quite a bit like date-fns did it.
This is effectively no different from Date:
in Temporal:And as far as I know, date-fns deals with native Date instances, not “data-only objects.”
This is known as the "primitive obsession" anti-pattern.
I mean, I guess it's two steps forward and one step back ... but couldn't they have come up with something that was just two steps forward, and none back ... instead of making us write this nightmare all over the place?
Why not?
However my guess is that the spec designers saw this lack of specivity as part of the problem.
A key issue of dates and times is that we use them culturally in day to day use in very imprecise ways and much is inferred from the context of use.
The concepts of zoned time and “wall clock” time are irreducable and it’s likely much code will be improved by forcing the developer to be explicit with the form of time they want to use and need for their particular use case.
I think this is why it’s so explicitly specified right now.
But I agree; I’ve often struggled with how verbose js can be.
Maybe with time (pun intended), more syntactic sugar and shorter conventions can be added to expand what has been an incredible effort to fix deep rooted issues.
There's also other methods that return other types, like
which isn't as bad.One could argue that the ugliness of the API intentionally reveals the ugliness of datetime. It forces you to really think about what you mean when you want "the current date time," which I think is one of the goals of the API.
And even with C#'s date API, I've seen errors. For example, a library that formatted datetime strings by merely writing them out and adding a "Z" to the end, assuming that they would always be receiving UTC datetimes — and elsewhere in the code, someone passing `DateTime.Now` to that library. (I'm guessing the dev who wrote that was in the UK and wrote it during winter time, otherwise he would have noticed that the timestamps were coming out wrong. If he was in the US they'd be 4-7 or 5-8 hours wrong depending on whether DST was in effect. But in the UK during winter, local time equals UTC and you might not notice that mistake).
This is another reason why Temporal's API making clear distinctions between the different types, and requiring you to call conversion functions to switch between them, is a good idea. That C# mistake would have been harder (not impossible, people can always misunderstand an API, but harder) if the library had been using Nodatime. And Temporal is based on the same design principles (not identical APIs, just simmilar principles) as Nodatime.
Like the Temporal.Instant with the only difference that is now but in nanosecond. Would have been better to be Now with a suffix to indicate that it is more precise. Or even better, it is just the function you use or parameter that give the precision.
And why Now as the name space? I would expect the opposite, like python, you have something like Temporal.Date, and from there you get a date of now or a specific time, with or without timezone info, ...
You raise a very important question: why is `Temporal.Now.zonedDateTimeISO()` so verbose? After 10 years of work on Temporal, you can assume that every API design and naming decision has been argued about, often many times. I'll try to summarize the reasoning and history below to explain how we got there.
There's really five questions here. There's no perfect order to answer them because the answers overlap somewhat, so you may want to read to the end until disagreeing. :-) I will have to split this response up into two posts because it's long.
1. Why do we need `Temporal.Now.`? Why not just `Temporal.`?
The TC39 members working on security required us to have a forward-compatible way to override and/or prevent access to information about the local machine. With the current design, a hosting environment can monkey-patch or remove `Temporal.Now`, and regardless of what Temporal changes in the future that local-machine info will still be blocked or overridden. But if we'd spread methods across each Temporal type (e.g. `Temporal.PlainDate.today()`, `Temporal.Instant.now()`) then a new Temporal type added in the future could allow jailbreaking to get local machine info.
This isn't just a security concern. Environments like jest may want to control the local time that tests see. Hosters of JS code like https://convex.dev/ might want to override the local time so that transactions can be re-run deterministically on another node. And so on.
2. Why a `Temporal.*` namespace? Why not just `ZonedDateTime`, `PlainTime`, etc. in the global namespace?
If `Date` didn't exist, we'd probably have done exactly this, and argued harder about (1) above, and we maybe could have won that argument!
But we were worried about the confusion between `Date` and another type named `PlainDate` (or whatever we could have called the date-only type). By putting all Temporal types in a namespace, it was much harder to be confused.
A secondary benefit of a namespace was to expose developers (via autocomplete in editors and browser dev tools) to the full set of types available, so that developers would pick the right type for their use case. But the `Date` conflict was the main reason.
We could have made the types TemporalDate, TemporalTime, etc. but that shaves only one character and violates (1) so there was no chance that would move forward. So we went with a namespace.
3. Why is it `Temporal.Now.zonedDateTimeISO()` not `Temporal.now()`?
As @rmunn and others discuss below, one of the core tenets of Temporal (following in the footsteps of Java, Noda time, Rust, and many others) was that developers must figure out up-front what kind of date/time data they want, and then pick the appropriate Temporal type for that data.
For a canonical example of why this is needed, just think of the thousands (millions?) of times that JS developers or end users have ended up with off-by-one-day errors because `new Date()` reports a different date depending on which time zone you're in, even if all you care about is displaying a date where the time zone is not relevant.
It's a reasonable argument that because `Temporal.ZonedDateTime` represents the superset of all Temporal types, it should occupy a privileged position and should be the default Temporal type for something like `Temporal.now()`. Given that I proposed the initial design of the ZonedDateTime type (https://github.com/tc39/proposal-temporal/pull/700) I'm understandably sympathetic to that point of view!
But there are performance concerns. Accessing the system time zone and calenda...
It's fine for distributed logging and computer-only usage, but fails in obscure ways once humans, time zones, travel, laws, and/or daylight saving time get involved.
If you're scheduling events for humans, and can't immediately list the reasons your app is an exception to the above, store the time zone to be safe. You probably don't have big data, and nobody will notice the minuscule overhead.
It's weird that they picked example code that is extremely non-accidentally doing this.
This is funny to me; Java's util.Date was almost certainly a port of C's time.h API!