-
-
Save MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054 to your computer and use it in GitHub Desktop.
// In TS, interfaces are "open" and can be extended | |
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; | |
} | |
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`; |
Excellent!
Yes! I might just need this 👌
Awesome. Thank you!
This is good (and similar to what I have been using) but it has one important gotcha that I wasn't aware of:
${number}
matches any number of digits and so the following still compiles:
const date: TDateISODate = '2023-03-123456789';
So using something like ${number}${number}
can guarantee a minimum number of digits, but it cannot guarantee an exact, or maximum number of digits.
And using something like:
type Digit = `0` | `1` | `2` | `3` | `4` | `5` | `6` | `7` | `8` | `9`;
type Year = `${Digit}${Digit}${Digit}${Digit}`;
Starts to blow up with the same Expression produces a union type that is too complex to represent. ts(2590)
error mentioned above when they are combined into larger types.
So because of this false sense of security, I'm less inclined to use this approach in the future, and I'm looking at other options such as runtime validation and type coercion as outlined in this video.
It's a bit more ceremony as you can't assign string literals directly to the types, but provides more safety.
Here's what I've been using:
type oneToNine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type d = oneToNine | 0;
type YYYY = `19${d}${d}` | `20${d}${d}`;
type MM = `0${oneToNine}` | `1${0 | 1 | 2}`;
type DD = `${0}${oneToNine}` | `${1 | 2}${d}` | `3${0 | 1}`;
type DateYMString = `${YYYY}-${MM}`;
type THours = `${number}${number}`;
type TMinutes = `${number}${number}`;
type TSeconds = `${number}${number}`;
type TMilliseconds = `${number}${number}${number}`;
// type: DateYMD ex. "2021-01-01"
export type DateYMD = `${DateYMString}-${DD}`;
/**
* type: TDateISOTime ex. "14:42:34.678"
*/
type TDateISOTime = `${THours}:${TMinutes}:${TSeconds}.${TMilliseconds}`;
/**
* type: DateYMDZoneString ex. "2021-01-01T14:42:34.678Z"
*/
export type ISO8601 = `${DateYMD}T${TDateISOTime}Z`;
My aim was not to make this perfect but to help point people in the right direction.
This approach is somewhat flawed. ISODate isn't a type—it's a format. The actual type would still be a string.
According to the ISO 8601 standard, timezones can include a Z
to indicate UTC or an offset.. For example, you could define a type like this:
type TDateISODateTimeZone = 'Z' | `+${THours}:${TMinutes}` | `-${THours}:${TMinutes}`;
The Offset can actually be empty (eg. 2021-01-01T14:42:34.678
) which is implicit +0 = Z.
That said, it's important to differentiate between types and validators. What you’re really looking for here is a validator, such as:
// sadly will say true for `2021-01-01T14:42:34.678ZASDFADFADF` as well
moment(value, moment.ISO_8601, true).isValid();
In my opinion, it's best to avoid relying heavily on ISODate formats. Instead, work with Date
objects in your application logic, and if you need to transfer data, use a more reliable format like epoch milliseconds (UTC).
I'm currently just using:
export function isValidISODate(value: string): value is ReturnType<typeof Date.prototype.toISOString> {
const byMoment: boolean = moment(value, moment.ISO_8601, true).isValid(); // tests only format, not if date is valid
const byJs: boolean = Number.isSafeInteger(new Date(value).getTime()); // getTime can return NaN or non-safe-integers but isSafeInteger doesnt allow non-safe-integer nor NaN nor Infinity.
return byJs && byMoment;
}
If Typescript ever decided to add a type for ISODate (highly doubt that) then this method would just return it as is.
Thanks 👍