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
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