Back Mon 2 Nov, 2020

Type | Treat: Challenge 5

Click here to see full details of the challenge.

The Challenge

  1. Use generic to reduce duplication
  2. 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

Link to my full 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 👋