Temporal, the new Date API in JavaScript
Mar 26, 2022
Handling dates in JavaScript has never been easy. The API is... not intuitive and there are frequent surprises, mutable objects being one of the culprits. All in all, it's a horrible system and an unfortunate legacy.
If you have been working anything at all with dates in JavaScript, chances are you have used one or several of the popular libraries that aim to bridge this difficult part of the language. One of them is moment.js that used to be the go to for almost everybody. But that package have grown too big because of it's success and now suffer it's own quirks. Nowadays most people turn to date-fns and I'd actually be surprised if I run into a project that deals anything with dates and doesn't depend on it.
But a new and exciting proposal is making the rounds and while it is still in the earlier stages (phase 3), it looks very promising and hopefully we'll see progress in the not too distant future. The name of it is the Temporal API which seems to put the standard dates to shame.
How does the Temporal API work?
The API is implemented through a new global object called Temporal
and it includes a bunch of new classes and functions. One of my immediate favourite parts is the support for working with dates without time. It seems like such a simple thing, but if you've ever tried working with something like that in JavaScript, then you'll know the struggle.
If you want the complete documentation, you can find it here, but I'll try to focus on a few interesting topics.
API Types in Temporal
The main thing I want to highlight about the different types in Temporal is that you do not have to deal with the parts of a date that is not relevant for your use case.
Don't need to consider time zones in your app? Then stick with the Plain types that Temporal provides.
Time is irrelevant in a scenario and is just an added overhead? No problem, Temporal has types for that.
You get the idea. Now let's take a look at some examples.
PlainDateTime
I believe the PlainDateTime
object is going to be the most frequently used date object since it simply deals with date and time (just like a normal date) but without the added complexity of dealing with time zones. To use it, simply call the plainDateTimeISO
function.
const today = Temporal.Now.plainDateTimeISO()
console.log(today.toString())
// => 2022-03-21T04:21:01.151783491
This will create a new PlainDateTime
instance that uses the current date and timestamp from your local timezone. Alternatively you can pass the timezone you want it to be relative to. But remember that this object does not deal with timezone so it is only used to resolve the correct time, but it does not store anything about the timezone in the object itself.
Another function available is the Temporal.Now.plainDateTime
function which you can use if you want to declare a specific calendar, but I don't expect this to be used as much.
const today = Temporal.Now.plainDateTime("chinese")
console.log(today.toString())
// => 2022-03-21T04:21:01.151783491[u-ca=chinese]
Now if you don't want to just use the current time, you can of course create a new object based on specific date values.
The base constructor for PlainDateTime
is one way to do it and it takes a year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, and calendar. It is only year, month and day that are required though.
const date = new Temporal.PlainDateTime(2022, 1, 1)
console.log(date.toString())
// => 2022-01-01T00:00:00
But with the new API comes a set of new ways to create instances. You can now also use the from
method on the PlainDateTime
object instead. You either pass a string that can be parsed as a date or an object with keys for each of the different parts of the date.
const date1 = Temporal.PlainDateTime.from("2022-03-03")
console.log(date1.toString())
// => 2022-03-03T00:00:00
const date2 = Temporal.PlainDateTime.from({ year: 2022, month: 3, day: 3 })
console.log(date2.toString())
// => 2022-03-03T00:00:00
As I'm sure you understand, all the above ways of creating new date instances, will work just the same for the other data types. Just keep that in mind as we move forward.
PlainDate
A PlainDate
object represents a date without and time or timezone. I can't tell you have many times I would have wanted something like this in the past.
const today = Temporal.Now.plainDateISO()
console.log(today.toString())
// => 2022-03-21
const date1 = Temporal.PlainDate.from("2022-01-31")
console.log(date1.toString())
// => 2022-01-31
const date2 = Temporal.PlainDate.from({ year: 2022, month: 1, day: 31 })
console.log(date2.toString())
// => 2022-01-31
const chinese = Temporal.Now.plainDate("chinese")
console.log(chinese.toString())
// => 2022-03-21[u-ca=chinese]
PlainTime
A PlainTime
is the reverse from the PlainDate
in that it only represents time without any date. Because of this, there is no Temporal.Now.plainTime
function because a time does not relate to any calendar.
const today = Temporal.Now.plainTimeISO()
console.log(today.toString())
// => 2022-03-21T04:21:01.151783491
const time1 = Temporal.PlainTime.from("01:23:45")
console.log(time1.toString())
// => 01:23:45
const time2 = Temporal.PlainTime.from({ hour: 1, minute: 23, second: 45 })
console.log(time2.toString())
// => 01:23:45
ZonedDateTime
A ZonedDateTime
is a probably what closest resembles the dates that we are used to work with, but hopefully you'll find this object and it's helper functions easier to work with. It has all the date, time and timezone components and can handle calculations daylight savings time, etc.
const today = Temporal.Now.zonedDateTimeISO()
console.log(today.toString())
// => 2022-03-21T04:21:01.151783491[Asia/Hong_Kong]
const chinese = Temporal.Now.ZonedDateTime("chinese")
console.log(chinese.toString())
// => 2022-03-21T04:21:01.151783491[Asia/Hong_Kong][u-ca=chinese]
const date1 = Temporal.ZonedDateTime.from("2022-01-31")
console.log(date1.toString())
// => 2022-01-31T00:00:00+08:00[Asia/Hong_Kong]
const date2 = Temporal.ZonedDateTime.from({ year: 2022, month: 1, day: 31 })
console.log(date2.toString())
// => 2022-01-31T00:00:00+08:00[Asia/Hong_Kong]
Instant
An Instant is similar to a ZonedDateTime
in that it represents a specific point in time, but it is always in UTC time and does not take into account any particular calendar. Neither can you pass an object to the from
method for an Instant and when you pass a string to the from method it must include timezone information.
const today = Temporal.Now.instant()
console.log(today.toString())
// => 2022-03-21T20:21:01.151783491Z
const date = Temporal.Instant.from("2022-01-31+08:00")
console.log(date.toString())
// => 2022-01-30T16:00:00Z
PlainMonthDay
A PlainMonthDay
is the same as PlainDate
, except it does not include a year. You'd typically use this to store records of public holidays or someone's birthday if they don't want to disclose a specific year, etc.
const date = Temporal.PlainMonthDay.from("01-01")
console.log(date.toString())
// => 01-01
const anotherDate = Temporal.PlainMonthDay.from({ month: 2, day: 10 })
console.log(anotherDate.toString())
// => 02-10
PlainYearMonth
This data type is one I personally don't think will be used a lot. I can see some use cases when you want to have representations of something happening all throughout a month, or perhaps a document (invoice) that might include all work that was carried out during a particular month. But it would be very situational.
const date = Temporal.PlainYearMonth.from("2022-01")
console.log(date.toString())
// => 2022-01
const anotherDate= Temporal.PlainYearMonth.from({ year: 2022, month: 1 })
console.log(anotherDate.toString())
// => 2022-01
TimeZone
The TimeZone
data type is used to represent a specific timezone.
const myTimeZone = Temporal.Now.timeZone()
console.log(myTimeZone.toString())
// => Asia/Hong_Kong
const fromTimeZone = Temporal.TimeZone.from('Europe/Stockholm')
console.log(fromTimeZone.toString())
// => Europe/Stockholm
Duration
The Duration
data type is used to represent a duration of time. You will probably not create an instance of this manually, but instead is something that you might get as you compare different dates. But if there is a need for it, you can create a new Duration
directly as well.
const duration = Temporal.Duration.from({ days: 4, months: 5 })
console.log(duration.toString())
// => P5M4D
Similarly to the above data types you can use the add, subtract, with, and round methods on durations. There are also a few additional helper methods that you will want to know.
const duration = Temporal.Duration.from({ hours: 10, minutes: 22 })
console.log(duration.total("minutes"))
// => 622
console.log(duration.negated().toString())
// => -PT10H22M
console.log(duration.negated().abs().toString())
// => PT10H22M
That covers most of the data types available. Now let's get to the fun parts!
Helper Methods
Once you have instances of the data types we have been talking about, chances are you want to do something with it, being comparing two of them or making changes like adding or subtracting from it.
There are a number of helper methods available and we'll go through a few of them. I'm just going to say one more time though that all of these objects are immutable (yay!) so every helper method leaves the original instance unchanged and instead returns a new object instance.
add
/ subtract
Finally there is an easy way of adding and subtracting time to a date. Let's get right to it!
Just like the from
functions you've seen before, simply pass an object where the keys are the units.
const today = Temporal.Now.plainDateISO()
console.log(today.add({ days: 4, months: 2 }).toString())
// => 2022-04-25
It even gives you the control of how you want to handle something like overflow.
const date = Temporal.PlainDate.from("2022-01-31")
console.log(date.add({ months: 1 }).toString())
// => 2022-01-28
date.add({ months: 1 }, { overflow: "restrict" })
// => Uncaught RangeError: value out of range: 1 <= 31 <= 28
It also accepts a string or a Temporal.Duration
object as an argument if you prefer to use that.
const today = Temporal.Now.plainDateISO()
console.log(today.add("P1D").toString())
// => 2022-03-22
const duration = Temporal.Duration.from({ days: 1 })
console.log(today.add(duration).toString())
// => 2022-03-22
since
/ until
Two convenient ways of comparing two temporal objects. As you might have guessed, these functions will return a Temporal.Duration
object.
const today = Temporal.Now.plainDateISO()
const yesterday = today.subtract({ days: 1 })
console.log(today.since(yesterday).toString())
// => P1D
const today = Temporal.Now.plainDateISO()
const lastMonth = today.subtract({ months: 1, days: 4 })
console.log(today.since(lastMonth).toString())
// => P32D
console.log(today.since(lastMonth, { largestUnit: "months" }).toString())
// => P1M4D
equals
Not much to say about this function really. Two separate objects would of course fail a strict equal comparison so instead we have this function to compare the object values and see if they are the same.
const today = Temporal.Now.plainDateISO()
const today2 = Temporal.Now.plainDateISO()
console.log(today === today2)
// => false
console.log(today.equals(today2))
// => true
with
Another convenient function. Based on a temporal object, replace only certain parts of it and keep the rest.
const today = Temporal.Now.plainDateISO()
console.log(today.with({ year: 2050, month: 9 }).toString())
// => 2050-09-21
round
This one is also obvious what it does, but so useful.
const today = Temporal.Now.plainDateTimeISO()
console.log(today.round("hour").toString())
// => 2022-03-22T04:00:00
You can also pass an object that takes smallestUnit
, roundingIncrement
and roundingMode
as parameters to configure how the rounding should be performed.
const today = Temporal.Now.plainDateTimeISO()
console.log(today.round({ smallestUnit: "hour" }).toString())
// => 2022-03-22T04:00:00
console.log(today.round({ smallestUnit: "hour", roundingMode: "ceil" }).toString())
// => 2022-03-22T05:00:00
console.log(today.round({ smallestUnit: "hour", roundingIncrement: 6 }).toString())
// => 2022-03-22T02:00:00
compare
The last method I want to talk about is the compare method which is available on the actual data type and not the object instance. This method is pretty much purely used for making sorting dates easier.
const today = Temporal.Now.plainDateISO()
const yesterday = today.subtract({ days: 1 })
const tomorrow = today.add({ days: 1 })
console.log([today, yesterday, tomorrow].sort(Temporal.PlainDate.compare))
// => ['2022-03-20', '2022-03-21', '2022-03-22']
Browser Support
Unfortunately, now is time to give you the bad news. The fact that this proposal still is in phase 3 means that there are still implementation questions to work out and as far as I know, no major browser supports this yet, so if you want to play around with it, you have to add a polyfill.
There seems to be a few polyfills that'll make this API available to use, but the one I have seen recommended the most seem to be @js-temporal/polyfill. So go ahead and install that if you want to give it a try.
That's it!
Maybe this will give you something to hope and look forward to.
Thanks for reading and Happy Coding!