DEV Community

Cover image for Fixing TypeScript Dates
Pedro Figueiredo
Pedro Figueiredo

Posted on

Fixing TypeScript Dates

Dates formats and timezones have for many years and probably for many more to come, been the nightmare of many developers and also the root of a lot of bugs in very different systems.

Dates in TypeScript

By typing a variable as Date in TypeScript you are defining that:

1- it is an object
2- it represents a Date
3- it has access to a lot of date-related methods- e.g. getDate

However, there are still two very important pieces of information that aren't defined in any way by the type system:

1 - What's the date's timezone?
2 - What's the date's format?

Narrowing dates information

Unfortunately, for the timezone, there isn't much that TypeScript can do for us, because it would be "impossible" for a static analysis tool to understand from a given "date string", which timezone it represents, at least without any further info or complex runtime validation.

However, for a date's format, TypeScript can be very helpful and provide just enough narrowing for us to validate if a "date string" represents a given format, all within the type system! Here is a brief example of how we could define a type for the European date string format:



// European DD/MM/YYYY
type EuropeanDate = `${number}${number}/${number}${number}/${number}${number}${number}${number}`;

const europeanDate: EuropeanDate = "20/10/2024"; βœ…


Enter fullscreen mode Exit fullscreen mode

Eventually, this may lead to some false positives, like the "MM/DD/YYYY" (American format), but type-wise, those formats are the same, so there isn't much we can do there.

Dates in Data Contracts

A data contract outlines how data can get exchanged between two parties. atlan

Data contracts are in it's essence, a way to communicate what data a given resource is going to deliver and expects to receive.

In the TypeScript ecosystem, having these contracts well-defined (or even auto-generated) is a must, as a lot of times clients rely on them to make requests and manipulate/present the response data.

But date types are usually a bit different from the rest of the data, due to their sensibility, they usually need to be passed around in the same format, but how can we know in which format, if the contract is defined like this:



type CreateTask = {
  name: string;
  // date in ISO format (is it?)
  dueDate: string;
}

type GetTask = {
  name: string;
  // date in ISO format
  dueDate: string;
}


Enter fullscreen mode Exit fullscreen mode

As you can see above, it's very common that we add comments pointing out what's the actual date format, due to the absence of a better alternative.

Enforcing a date format

The most common date format used in IT systems is probably the ISO 8601, it is a standard date and time representation that ensures consistency and is easily sortable, also, it has very good support across different programming languages and systems.

  • Basic format: YYYY-MM-DD
  • Extended format: YYYY-MM-DDTHH:MM:SS

This means that ideally, our data contracts would also have all the date fields typed as ISO somehow.



type GetTask = {
  name: string;
  // date in the ISO format!
  dueDate: TDateISO;
}


Enter fullscreen mode Exit fullscreen mode

Defining the TDateISO type

The 1st step to have our date field typed properly, is obviously to define a TypeScript type that represents the ISO 8601 date string format.

This type was extracted from this gist.



type TYear         = `${number}${number}${number}${number}`;
type TMonth        = `${number}${number}`;
type TDay          = `${number}${number}`;
type THours        = `${number}${number}`;
type TMinutes      = `${number}${number}`;
type TSeconds      = `${number}${number}`;
type TMilliseconds = `${number}${number}${number}`;

/**
 * Represent a string like `2021-01-08`
 */
type TDateISODate = `${TYear}-${TMonth}-${TDay}`;

/**
 * Represent a string like `14:42:34.678`
 */
type TDateISOTime = `${THours}:${TMinutes}:${TSeconds}.${TMilliseconds}`;

/**
 * Represent a string like `2021-01-08T14:42:34.678Z` (format: ISO 8601).
 *
 * It is not possible to type more precisely (list every possible values for months, hours etc) as
 * it would result in a warning from TypeScript:
 *   "Expression produces a union type that is too complex to represent. ts(2590)
 */
type TDateISO = `${TDateISODate}T${TDateISOTime}Z`;


Enter fullscreen mode Exit fullscreen mode

As stated in the comments in the code block, this isn't bulletproof, but as of right now, it is as far as we can go, type-wise, and it's probably good enough for a data contract where you control both the frontend and the backend of the systems communication.

Overriding native toISOString

toIsoString returns string

As you can see above, the native JS Date prototype toISOString() isn't ready for this new type that we just created and in fact, it just returns a simple string. So, we must somehow override its return type.

Luckily for us, the Date type is defined in an interface, and interfaces can be "merged" with other interfaces due to declaration merging - meaning that we can create a new interface with the same name, and modify the existing types (within certain limits).



// add this to any TS module (file with exports/imports)
interface Date {
  /**
   * Give a more precise return type to the method `toISOString()`:
   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
   */
  toISOString(): TDateISO;
}


Enter fullscreen mode Exit fullscreen mode

Once that's done, anytime we use the native toISOString, will result in the TDateISO that we created in the previous step. πŸ‘‡

final result with toIsoString override

Summary

1- Standardize all the data contract's dates' format
2- Setup a new TS type that can represent that date format
3- Change the dates in the contracts to the new type
4- Narrow the return type of the native Date functions, if it applies to your system's date format

Conclusion

Dates don't have an easy time in the TypeScript ecosystem, due to so many different variations in formats and timezones, it is still pretty much impossible to have full type safety.

However, for date formats, it is already possible to narrow down the types enough, to get a much better and safer Developer Experience.


Make sure to follow me on twitter if you want to read about TypeScript best practices or just web development in general!

Top comments (1)

Collapse
 
jhpg profile image
Jorge Garcia

Great trick! It's a new one for me.
I was testing here and want to add that it only requires a minimum amount of numbers, but not a maximum.

In my tests, it's still valid:

const europeanDate: EuropeanDate = "200/0111/2024444";
Enter fullscreen mode Exit fullscreen mode

In any case, your solution is better than the vanilla TS, thanks!