v3 is out!

Published by Sasha Koss's avatar Sasha Koss

🎉 After many months of development, v3 is out!

For most developers, upgrading won’t require any changes. For some, it will be pretty trivial.

However, the release brings tons of good stuff, so for the starter, here’s a quick overview of the most notable changes:

  • The library is now 100% TypeScript with brand-new handcrafted types.
  • Removed arguments checking and conversion code from all functions, except for formatting and parsing, which resulted in a smaller minimal build size.
  • String date arguments are now back!
  • Support for Date class extensions like UTCDate.
  • ESM support for Node.js.
  • All functions now export via named exports, improving compatibility with different setups.
  • New flat library structure improving DX for ESM/Deno (node_modules/date-fns/add.mjs).
  • No more IE support.

If you want to jump right into it, see the v3.0.0 change log.

In this blog post, I’ll explain the changes in more detail and give context for the choices we made while working on v3.

TypeScript support

This release started as a rewrite to TypeScript. When I started working on the library in 2014, TypeScript seemed like a niche project.

Since then, the world has changed, and most developers have realized the power of types and largely adopted the language.

In date-fns, types were an afterthought generated from JSDoc. I didn’t really understand how it worked, and over the years, the generated types became a 23K LOC monster with no hope for an easy fix.

To ensure date-fns provides first-class TypeScript support, we rewrote hundreds of functions to TypeScript. We carefully handcrafted every type and interface. We exported everything, including options interfaces for every function, so you can easily access those.

It took a while, but it happened with the community’s help, and I’m very proud of the result.

Removed arguments checking

While migrating to TypeScript, I reevaluated the value of the argument checks we introduced in v2.

The idea behind them was to provide as much type safety to the runtime as possible. Checking the number of arguments and their types seemed like a good idea.

Now, with first-class TypeScript support, it seems redundant.

So, we removed as many checks as possible, leaving only the format and parse functions mostly intact.

Delegating type safety to TypeScript made the source code cleaner and improved the minimal build size.

Before, I was happy to bring the minimal build size down to 300 bytes. Now it’s just 200!

Strings as arguments

In v2, we removed strings as arguments. Now they are back.

My reasoning for the removal was that passing an invalid string and expecting it to get parsed correctly was a common mistake. Another widespread issue was passing the date without time and getting UTC midnight instead of the local timezone, causing bugs in the user code. In both cases, the developers would blame date-fns, putting the support burden on us.

However, it did not solve the issue for users. They would wrap strings in new Date before passing them to the library and get the bug anyway. But then thousands of developers had to change every function call that involved strings when migrating from v1 to v2, often missing a few places when working without types and getting bugs. Also, the new developers had extra friction working with the library.

I admit my mistake, and now the strings are back. It might cause an influx of new issues, but I’ll deal with them separately.

Support for Date extensions

Timezones support is one of the oldest feature requests I have masterfully avoided for years. I didn’t want to reimplement Moment.js IANA functionality as shipping a huge timezones database contradicts the goal of making the most lightweight date library for JavaScript.

However, I decided to do something about it finally and released @date-fns/utc that provides the UTCDate class which is an extension that maps regular functions like getHours to the UTC versions like getUTCHours.

That enabled date-fns to perform all calculations in UTC without changing its code. However, making it happen required overhauling how arguments are handled, so I postponed it until v3.

Also, thanks to TypeScript generics, if you pass the UTCDate date as an argument and expect a date back, you’ll get the UTCDate.

This change opens the door for more date extensions in the future.

ESM support for Node.js

Another big thing that has changed since the v2 release is that Node.js got ESM support. A simple problem on the surface became an enormous task requiring a build system overhaul.

In v3, I had to rewrite the build system almost entirely, so I tackled Node.js ESM as well.

date-fns is still a dual package with both CommonJS and ESM, but given how much effort is required to make it right, v4 will be ESM-only.

No more default exports

All functions used to export default, but while working on ESM support for Node.js and testing the library against Are the types wrong? I found it impossible to make it work in all possible setups due to the nature of how export default behaves.

So I decided to compromise and switch to named exports, so now you import functions from their submodules, you’ll need to change your code:

import addDays from "date-fns/addDays";
// ->
import { addDays } from "date-fns/addDays";

To my surprise, it broke Next.js. So, after some wrestling with it, I decided to add a fallback so that Next.js users don’t come to me complaining. It sucks, but there’s nothing I can do.

By the way, the Next.js team never did get back to me. Hello?!

Flat structure

Since I was reworking the build system, I decided to address the annoyance that browser ESM and Deno users had to deal with. The index files!

Before, the import looked like that: https://unpkg.com/date-fns/addDays/index.mjs

Now, the library structure is flat, so there are no more ugly index files: https://unpkg.com/date-fns/addDays.mjs

Neat, right?

Bye-bye IE

I shipped v2 four years ago, and IE still had its legs. Now it’s time to say goodbye!

IE was never a good boy.

Some history

The big theme of major date-fns releases is fixing the mistakes we made in the previous ones.

When working on the v1 API, we drew inspiration from many places, from Moment.js to Ruby on Rails. It resulted in an inconsistent API and approach to dealing with edge cases. So, when the time came, we decided to rework the library from the ground up, reviewing every bit of a hundred functions. I decided to take the most conservative route, between developer experience and making the API error-proof, always choosing the latter. We would use an existing standard, replacing familiar JS patterns where possible.

Back in the time, JavaScript fatigue was a big topic. New library versions, often incompatible, would be released every few months, making users endlessly refactor their work to keep their dependencies working. Since we had to make significant breaking changes, we packed changes worth a few major versions in a single release.

All that, of course, backfired. All started in long development cycles and endless “any updates,” “what’s the ETA” (and other less polite) comments. Many breaking changes, especially the ones that degraded the developer experience in favor of the “best” approach, caused an overwhelming amount of (justified) critique. Another side effect is that the library’s build size increased because of many guardrails.

While it was worth it, as in the end, we got highly consistent API, in v3, I decided to take a step back and make the API more developer-friendly. Having time to reflect, I now see which trade-offs were the least efficient.

Also, while having many ideas for improving the library that requires breaking changes, I decided to focus on a few and make smaller, iterative steps.

And it has still taken three years since I announced migration to TypeScript.

v4 and beyond

To stay relevant, date-fns must evolve and adapt to the ever-changing world of JavaScript.

While it’s a bit early to start working with Temporal, it’s time to embrace Intl fully.

format stays the most popular yet most heavy function in date-fns, which power most developers don’t need. Often, people use it to format an ISO date instead of using built-in functionality:

new Date().toISOString().slice(0, 10);
//=> '2023-12-18'

The rest can solve their problems with Intl.

So my goal for v4 is to extract I18n to separate packages like @date-fns/es and @date-fns/de and replace format, formatDistance, etc, with Intl alternatives intlFormat, intlFormatDistance.

I hope this will make people migrate to much lighter functions and reduce the build and package size significantly.

It’s famous last words, but this time, I promise it won’t take three years to finish v4.

I hope you’ll like what we did with v3. If you want to support the project, you can become my sponsor on GitHub.