Suppose out of the sudden you are dealing with an object, which besides well-known fields has a number of fields which are named after the day of the week, date and month, and contain some value related to that particular day. I.e. let's say you keep a record how many times you're happy to use Typescript
instead of pure Javascript
:
The final code is available in the playground.
const howHappyToUseTypescriptHistory = {
Thu29Jul: Infinity,
overall: Infinity
}
So far good, type is inferred, but what will happen on Friday? Need to add another record, recalculate the average, inferred type changes. Is there a way to type such construction at all?
Turns out, there is. Typescript
v4 provides superpowers to do that.
Let's start with creating type for weekdays:
type DaysOfTheWeek = 'Mon' | 'Tue' | 'Wen' | 'Thu' | 'Fri' | 'Sat' | 'Sun';
Now types for months, in accordance with how many days they have (omitting February):
type ThirtyOneDaysMonths = 'Jan' | 'Mar' | 'May' | 'Jul' | 'Aug' | 'Oct' | 'Dec';
type ThirtyDaysMonths = 'Apr' | 'Jun' | 'Sep' | 'Nov';
All months can have 29-30-31 days top, so that's 3 sets of types using Typescript
's template literals:
type OneThroughNine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type OneThroughTwentyNine = `${0 | 1 | 2}${OneThroughNine}`;
type OneThroughThirty = OneThroughTwentyNine | '30';
type OneThroughThirtyOne = OneThroughThirty | "31";
At this point you've probably guessed that we're simply going to use a generic now to glue all these strings together into our DayDateMonth
format:
type DayOfWeekDateMonth<DaysInMonth extends string, Months extends string> = `${DaysOfTheWeek}${DaysInMonth}${Months}`;
type DayDateMonth = DayOfWeekDateMonth<OneThroughTwentyNine, 'Feb'> | DayOfWeekDateMonth<OneThroughThirty, ThirtyDaysMonths> | DayOfWeekDateMonth<OneThroughThirtyOne, ThirtyOneDaysMonths>;
With the type for our strings ready, we can define a generic record to store data for different days:
type DayDateMonthRecord<T> = {
[key in DayDateMonth]?: T;
};
A mapped type may not declare its own properties or methods, so in order to add average
or any other field, we would need to extend DayDateMonthRecord<T>
:
interface ListWithAverage<T> extends DayDateMonthRecord<T> {
average: number;
}
Finally, we can type the initial object:
const howHappyToUseTypescriptHistory: ListWithAverage<number> = {
Thu29Jul: Infinity,
overall: Infinity
}
Now we have strict typing with intellisense if needed :)