Click here to see full details of the challenge.
The Challenge
- Use generic to reduce duplication
- Improve types to highlight invalid function calls
1. Use generic to reduce duplication
Given the following types, create a generic House
type which can be used to remove duplication of properties
type FirstHouse = {
doorNumber: 1;
trickOrTreat(): "book" | "candy";
restock(items: "book" | "candy"): void;
};
type SecondHouse = {
doorNumber: 2;
trickOrTreat(): "toothbrush" | "mints";
restock(items: "toothbrush" | "mints"): void;
};
type ThirdHouse = {
doorNumber: 3;
trickOrTreat(): "candy" | "apple" | "donuts";
restock(items: "candy" | "apple" | "donuts"): void;
};
My Solution
The challenge guides us to create a generic House
with 1 argument, to cover the return value of trickOrtreat()
and the argument to restock
.
type House<T> = {
trickOrTreat(): T;
restock(items: T): void;
};
// allows us to update the houses to
type FirstHouse = { doorNumber: 1 } & House<"book" | "candy">;
type SecondHouse = { doorNumber: 2 } & House<"toothbrush" | "mints">;
type ThirdHouse = { doorNumber: 3 } & House<"candy" | "apple" | "donuts">;
I decided to go a step further and include the doorNumber
in the generic, for greater succinctness, to end up with
type House<N extends number, T> = {
doorNumber: N;
trickOrTreat(): T;
restock(items: T): void;
};
type FirstHouse = House<1, "book" | "candy">;
type SecondHouse = House<2, "toothbrush" | "mints">;
type ThirdHouse = House<3, "candy" | "apple" | "donuts">;
2. Improve types to highlight invalid function calls
Given the following data and function
const moviesToShow = {
halloween: { forKids: false },
nightmareOnElmStreet: { forKids: false },
hocusPocus: { forKids: true },
theWorstWitch: { forKids: true },
sleepyHollow: { forKids: false },
} as const;
function makeScheduler(movies: typeof moviesToShow): any {
const schedule = {} as any;
for (const movie in Object.keys(movies)) {
const capitalName = movie.charAt(0).toUpperCase() + movie.slice(1);
schedule[`getVHSFor${capitalName}`] = () => {};
schedule[`makePopcornFor${capitalName}`] = () => {};
schedule[`play${capitalName}`] = () => {};
}
return schedule;
}
ensure that the compiler warns us of invalid function calls
// these functions are automatically created from the movies
movieNight.getVHSForHalloween();
movieNight.makePopcornForHalloween();
movieNight.playHalloween();
// but this is not a halloween movie! So this should raise a compiler error
movieNight.getVHSForNightmareBeforeChristmas();
movieNight.makePopcornForNightmareBeforeChristmas();
movieNight.playNightmareBeforeChristmas();
My Solution
Link to first part of my solution
As in a previous refactoring challenge, the first step was to lift out existing type definitions out to standalone types
type Movies = typeof moviesToShow;
function makeScheduler(movies: Movies): any {
/* ... */
}
Then, we know we are going to need the capitalized movie names, this line in makeScheduler
tells us as much
const capitalName = movie.charAt(0).toUpperCase() + movie.slice(1);
To obtain the capitalized names we can use template literal types again, which we encountered in the previous challenge
type MovieTitles = `${capitalize (keyof Movies)}`;
// gives us
"Halloween" | "NightmareOnElmStreet" | "HocusPocus" | "TheWorstWitch" | "SleepyHollow"
Now we can construct the list of valid function calls that can be made on a scheduler. Another template literal type comes in handy here, though this time it is a generic to boot
type SchedulerAPI<Title extends string> =
| `getVHSFor${Title}`
| `makePopcornFor${Title}`
| `play${Title}`;
This type will allow us to generate all the valid function names from the movie titles that are available. We create that full list via
type Schedule = Record<SchedulerAPI<MovieTitles>, () => {}>;
We now have a type which should alert us to the errors that we were tasked with uncovering. To see if this is the case, we update the return type of the makeScheduler
function from any
to Schedule
function makeScheduler(movies: Movies): Schedule {
/* ... */
}
And we now see the compiler warning us of errors on the following lines
// nightmareBeforeChristmas is not in moviesToShow so this rightfully errors
movieNight.getVHSForNightmareBeforeChristmas();
movieNight.makePopcornForNightmareBeforeChristmas();
movieNight.playNightmareBeforeChristmas();
// typo! the movie is called Hocus Pocus, not Hocus Focus
movieNight.getVHSForHocusFocus();
Aside
After this, I ended up down a bit of a rabbit hole. I tried to update the type of the schedule
variable in the makeScheduler
function as follows
// from
const schedule = {} as any;
// to
const schedule = {} as Schedule;
But this raised new errors. The lines being complained about were
schedule[`getVHSFor${capitalName}`] = () => {};
schedule[`makePopcornFor${capitalName}`] = () => {};
schedule[`play${capitalName}`] = () => {};
// the following error was raised
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type Schedule
I think understand what is going on here, but I was at a loss as to how to resolve it. The complaint is that the property access in these lines is using a string (the value getVHSFor${capitalName}
is a string), whereas the properties on the Schedule
type are not a string. I couldn't figure out what type these actually were, or how to make them a string, and after an inordinate amount of time trying to figure out what to do I eventually bailed and moved on by reverting schedule
back to any
. 😬
And as for part 2 of the challenge, it broke me. It's possible that my solution to part 1 didn't help me, which perhaps points to some flaws in it. In any case, after consulting the solution (again), I modified my solution to part 1, which then enabled me to implement the extra conditional required to solve part 2. I'm pretty sure I wouldn't have been able to solve it regardless of how much time I spent, so there's probably a bunch of stuff for me to read up on or continue to practice. For completeness sake, here is the link to my updated solutions for this challenge.
Wrapping Up
The intermediate/advanced challenge on this one was probably at or beyond the limit of my knowledge and skill level. Which in part, was the point of doing these exercises, so that I could both test my abilties and highlight areas to focus on going forwards.
Overall, I've enjoyed these challenges, despite the frustrations of this last one. Any questions or feedback or advice, on this challenge, or all of them in the series, hit me up on Twitter. Thanks, and cya 👋