Temporal, the new Date API in JavaScript

Temporal, the new Date API in JavaScript

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. You also cannot 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!

Did you find this article valuable?

Support Daniel Viklund by becoming a sponsor. Any amount is appreciated!