Working with Dates in TypeScript

#The problem

Let's imagine very simple situation. You need an object where keys are dates and values are strings strictly binded with key date.

Example:


const data = {
  '2021-03-01': {
    "date": '1st March',
    "value": 17
  },
  '2021-03-02': {
    "date": '2nd March',
    "value": 19
  },

  '2021-03-09': {
    "date": '9th March',
    "value": 15
  }
}

First solution which came to my mind was to make a simple Record:

Record<string, { date: string, value: number }>

But, since we have template literals, let's make it more complicated

#Template literals and Date

First of all, in order to work with dates, we should generate numbers from 1 to 12 and from 1 to 31. I hope you understand my intensions ))

This code was shamelessly stolen fromhere


type PrependNextNum<A extends Array<unknown>> = A["length"] extends infer T
    ? ((t: T, ...a: A) => void) extends (...x: infer X) => void
    ? X
    : never
    : never;

type EnumerateInternal<A extends Array<unknown>, N extends number> = {
    0: A;
    1: EnumerateInternal<PrependNextNum<A>, N>;
}[N extends A["length"] ? 0 : 1];

type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[]
    ? E
    : never;

If you are interested in really big ranges,this article is for you.

I'm sorry, let's go back to our problem.

Next, we have to create stringified representation of number, because we are working with strings.


type NumberString<T extends number> = `${T}`;

Now, we are ready to create our type representation of year


type Year =
    `${NumberString<number>}${NumberString<number>}${NumberString<number>}${NumberString<number>}`;

Let's create allowed numbers for months.


 type Range = Enumerate<13>

Now, we should get rid of zero


 type Month = Exclude<Enumerate<13>, 0>

We all know, that according to standard, we should use zero lead representation for dates.

For example, we should use 02 for February instead of2

First of all, we should define all numbers where we should add leading zero


type ZeroRequired = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

Now, we can add zero to all one digit numbers


type AddZero<T extends number> = T extends ZeroRequired ? `${0}${T}` : T;

Let's try what we have.


type Month = AddZero<Exclude<Enumerate<13>, 0>>;

Now we have a mix of numbers and string

We are interested only in stringified numbers


type MakeString<T extends number | `${number}`> = `${T}`;

Omg, finally we can make our Month type representation


type Month = MakeString<AddZero<Exclude<Enumerate<13>, 0>>>;

Now, it is easy to make Day type


type Day = MakeString<AddZero<Exclude<Enumerate<32>, 0>>>;

Our object key type:


type DataKey = `${Year}-${Month}-${Day}`;

Since we already have types for our keys, we should somehow map them. For example 01-01-2020 to January 1st


type MapMonth<T extends NumberString<number>> =
    T extends '01'
    ? 'January' : T extends '02'
    ? 'February' : T extends '03'
    ? 'March' : T extends '04'
    ? 'April' : T extends '05'
    ? 'May' : T extends '06'
    ? 'June' : T extends '07'
    ? 'July' : T extends '08'
    ? 'August' : T extends '09'
    ? 'September' : T extends '10'
    ? 'October' : T extends '11'
    ? 'November' : T extends '12'
    ? 'December' : never

Now, we should be able to obtain actual month and day from the key


type GetDay<T extends DataKey> =
    T extends `${string}-${Month}-${infer D}` ? D : `${number}`;

type GetMonth<T extends DataKey> =
    T extends `${string}-${infer M}-${Day}` ? M : `${ number }`;

Now we should actually convert two digits month to month string representation


type ConvertToMonth<T extends DataKey> = MapMonth<GetMonth<T>>;
type Result = ConvertToMonth<'2021-03-01'> // March

Do you remember that we should convert 2021-03-01 to1st March ?


type AddSt<T extends NumberString<number>> = `${T}st`;

type RemoveLeadZero<T extends GetDay<DataKey>> = T extends `0${infer N}` ? N : T

type MakeDate<T extends DataKey> =
    `${AddSt<RemoveLeadZero<GetDay<T>>>} ${ConvertToMonth<T>}`

This is how our type should be represented


type Base = Record<DataKey, { date: MakeDate<DataKey>, value: number }>

type Result = MakeDate<'2021-03-01'> // 1st March
type Result2 = MakeDate<'2021-03-02'> // 2st March

There is a drawback, 2st March. Making appropriate endings is up to you :)

Full example


type PrependNextNum<A extends Array<unknown>> = A["length"] extends infer T
    ? ((t: T, ...a: A) => void) extends (...x: infer X) => void
    ? X
    : never
    : never;

type EnumerateInternal<A extends Array<unknown>, N extends number> = {
    0: A;
    1: EnumerateInternal<PrependNextNum<A>, N>;
}[N extends A["length"] ? 0 : 1];

type Enumerate<N extends number> = EnumerateInternal<[], N> extends (infer E)[]
    ? E
    : never;

type Range = Enumerate<13>

type NumberString<T extends number> = `${T}`;

type Year =
    `${NumberString<number>}${NumberString<number>}${NumberString<number>}${NumberString<number>}`;

type ZeroRequired = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

type AddZero<T extends number> = T extends ZeroRequired ? `${0}${T}` : T;

type MakeString<T extends number | `${number}`> = `${T}`;

type Month = MakeString<AddZero<Exclude<Enumerate<13>, 0>>>;

type Day = MakeString<AddZero<Exclude<Enumerate<32>, 0>>>;

type DataKey = `${Year}-${Month}-${Day}`;

type MapMonth<T extends NumberString<number>> =
    T extends '01'
    ? 'January' : T extends '02'
    ? 'February' : T extends '03'
    ? 'March' : T extends '04'
    ? 'April' : T extends '05'
    ? 'May' : T extends '06'
    ? 'June' : T extends '07'
    ? 'July' : T extends '08'
    ? 'August' : T extends '09'
    ? 'September' : T extends '10'
    ? 'October' : T extends '11'
    ? 'November' : T extends '12'
    ? 'December' : never
type GetDay<T extends DataKey> =
    T extends `${string}-${Month}-${infer D}` ? D : `${number}`;

type GetMonth<T extends DataKey> =
    T extends `${string}-${infer M}-${Day}` ? M : `${ number }`;

type ConvertToMonth<T extends DataKey> = MapMonth<GetMonth<T>>;

type Result = ConvertToMonth<'2021-03-01'> // March

type AddSt<T extends NumberString<number>> = `${T}st`;

type RemoveLeadZero<T extends GetDay<DataKey>> = T extends `0${infer N}` ? N : T

type MakeDate<T extends DataKey> =
    `${AddSt<RemoveLeadZero<GetDay<T>>>} ${ConvertToMonth<T>}`

type Base = Record<DataKey, { date: MakeDate<DataKey>, value: number }>

type Result2 = MakeDate<'2021-03-01'> // 1st March

type Result3 = MakeDate<'2021-03-02'> // 2st March

Playground

March 21, 2021