Fixing TypeScript Dates
Tags: javascript, typescript, webdev
Published: 2024-01-14 18:27:57

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:

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

const europeanDate: EuropeanDate = "20/10/2024"; ✅
``` 

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](https://atlan.com/data-contracts/)

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:

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

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

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.

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

## 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](https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054).

```ts
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`;
``` 
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0gzw0l0ejwl9b42qfjcu.png)

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](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) - meaning that we can create a new interface with the same name, and modify the existing types (within certain limits).

```ts
// 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;
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3hergtfjs7ko68wa8gf.gif)

## 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](https://twitter.com/P_FFigueiredo) if you want to read about TypeScript best practices or just web development in general!_