TypeScript - Type | Treat Halloween Challenge
Ethan Arrowood
Posted on November 1, 2020
Type | Treat Challenge
This halloween, the TypeScript team has released a series of challenges for us to solve. In this post I'll be going over all of my solutions to the challenges.
If you have any questions about my solutions or want to share your own, comment below!
The day I was planning on publishing this article, I realized the TypeScript team posted their own solutions for each of the challenges. If taken a bit extra time to expand upon my answers and try to provide additional information about the TypeScript patterns used in the solutions.
Challenge 1
Try it for yourself here
Beginner/Learner - Ghosts and Hauntings
The first challenge solution requires replacing the any
argument in displayHauntings
. The previous method displayGhost
uses the displayHauntings
method to print out the array of hauntings defined by the GhostAPIResponse
. Unfortunately, the hauntings type is inlined with GhostAPIResponse
interface and the challenge specifies not to modify GhostAPIResponse
.
My first solution is to infer the item type of GhostAPIResponse.hauntings
array using a custom utility type:
type ExtractArrayType<Arr> = Arr extends (infer Element)[] ? Element : never
To better understand this utility, lets break it down piece by piece. The type itself has a single generic Arr
. The TypeScript interpreter attempts to match the generic with the type (infer Element)[]
. This type uses the infer
keyword to specify a second generic, Element
. The interpreter is smart enough to assign whatever the item type of Arr
is to the generic Element
, and then since the interpreter has successfully matched the generic Arr
to the type (infer Element)[]
it returns the inferred element type Element
. And if the interpreter is not able to match Arr
, the utility returns never
; preventing misuse.
Finally, to complete the challenge I replaced any
with ExtractArrayType<GhostAPIResponse['hauntings']>
, and discovered that there was in fact a type error on line 41 (provemance
-> provenance
).
Check out my challenge solution playground
Looking back on my first solution, I wouldn't consider the infer
keyword something a beginner/learner would know. So, there had to be another solution. This got me thinking more about the Array
type.
The Array
interface is defined here in lib.es5.d.ts. It has one generic T
, and the definition has an index type of [n: number]: T
.
Using the index type definition, the solution can be simplified to: GhostAPIResponse['hauntings'][number]
. No inference necessary!
Here is the updated solution playground.
Intermediate/Advanced - Trick or Treat
This challenge was a blast for me. One of my favorite math subjects is set theory.
The challenge starts with a union of halloween tricks or treats, ResultsFromHalloween
. Each item in the union has either a candy: true
property or a trick: true
property. The first step in the challenge is to sort the union between tricks and treats.
Luckily, there is a built-in utility type.
Extract<Type, Union>
constructs a type by extracting fromType
all union members that are assignable toUnion
.
type AllCandies = Extract<ResultsFromHalloween, {candy: true}>
type AllTricks = Extract<ResultsFromHalloween, {trick: true}>
Under the hood, the Extract
type utilizes conditional types:
type Extract<T, U> = T extends U ? T : never
For each discrete item in T
, the interpreter checks if it satisfies the conditional T extends U
, if it does then that item is added to the resulting union.
The challenge has one more task, create another type that excludes candies with peanuts in them. But, the treat types only describe if they have peanuts in them, not if they don't. Now the solution requires the inverse of extracting, excluding.
More set theory! Yay!
type AllCandiesWithoutPeanuts = Exclude<AllCandies, {peanuts: true}>
Yet again, TypeScript has a utility type for this operation.
Exclude<Type, ExcludedUnion>
constructs a type by excluding fromType
all union members that are assignable toExcludedUnion
.
This one also uses conditional types:
type Exclude<T, EU> = T extends EU ? never : T
For each discrete item in T
, the interpreter checks if it satisfies the conditional T extends U
, if it doesn't only then is that item added to the resulting union.
Check out my challenge solution playground
Challenge 2
Try it for yourself here
Beginner/Learner - Pumpkin Patch
This challenge tasks us to make use of the typeof
operator. TypeScript is quite smart as long as you provide it with the right information. For the first part of this challenge the solution is to derive the Pumpkin
type from the pumpkin
object already defined using typeof pumpkin
. The second part of the challenge uses a TypeScript utility type ReturnType<F>
. The solution here is to pass the derived type of the function to the ReturnType
utility: ReturnType<typeof createExamplePumpkin>
.
To better understand how ReturnType
works take a look at the implementation
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
The type specifies a generic T
that is constrained to a function with any
arguments and any
return value (i.e. (...args: any) => any
). This generic T
is then matched against the type (...args: any) => infer R
. Here, the interpreter infers the return value of the function and assigns it the parameter R
. If this match is successful, then the type R
is returned.
Check out my challenge solution playground
Intermediate/Advanced - Ghosts, Gods, and Demons oh my!
Stay on your guard.
Thank you challenge author, hint received! The solution for this challenge makes use of type guards. The challenge tasks us to create three user-defined type guards areGods
, areDemons
, and areEctoPlasmic
.
My initial thought was to define three new types and the three functions:
type Gods = unknown
function areGods(ghosts: Ghosts[]): ghosts is Gods[] {}
type Demons = unknown
function areDemons(ghosts: Ghosts[]): ghosts is Demons[] {}
type EctoPlasmic = unknown
function areEctoPlasmic(ghosts: Ghosts[]): ghosts is EctoPlasmic[] {}
User-defined type guards can be unsafe if not used correctly. Whenever I create type guards, I try to think in absolutes. For example, the areGods
guard must only return true
if all of the ghosts
are gods. This is true for other type guards as well. Since the ghosts
argument is an array, we can use an ES6 method Array.prototype.every
to validate that every item in the array satisfies a condition. In this case, the condition is the existance and truthiness of the god
, demon
, or ectoplasmic
properties. Additionally, similar to the previous challenge, the three types can be defined using Extract
and the Ghosts
type union.
type Gods = Extract<Ghosts, { god: true }>
function areGods(ghosts: Ghosts[]): ghosts is Gods[] {
return ghosts.every(ghost => "god" in ghost && ghost.god)
}
type Demons = Extract<Ghosts, { demons: true }>
function areDemons(ghosts: Ghosts[]): ghosts is Demons[] {
return ghosts.every(ghost => "demon" in ghost && ghost.demon)
}
type EctoPlasmic = Extract<Ghosts, { ectoplasmic: true }>
function areEctoPlasmic(ghosts: Ghosts[]): ghosts is EctoPlasmic[] {
return ghosts.every(ghost => "ectoplasmic" in ghost && ghost.ectoplasmic)
}
In my solution, not only do I check for the existance of the relative property in the item, but also if that property is truthy. Since this challenge only ever uses true
the truthy check is unnecessary.
Check out my challenge solution playground
Challenge 3
Try it for yourself here
Beginner/Learner - House Types
Starting off, the challenge tasks us to define three types that correspond to the three arrays:
type TreatHouse = {
location: string,
result: 'treat',
treat: {
candy: string,
baked?: string,
},
}
type TrickHouse = {
location: string,
result: 'trick',
trick: string,
}
type NoShowHouse = {
location: string,
result: 'no-show',
}
The challenge then leads us to simplifying these types. Two properties location
and result
exist in each type, and the result
type is only three literal values. So extrapolate that first:
type House = {
location: string,
result: 'treat' | 'trick' | 'no-show',
}
Next, intersect the House
type with the previous three types and remove the location
property. I kept the result
property specified to the corresponding literal.
type TreatHouse = House & {
result: 'treat',
treat: {
candy: string,
baked?: string,
},
}
type TrickHouse = House & {
result: 'trick',
trick: string,
}
type NoShowHouse = House & {
result: 'no-show'
}
In a real-code examples this pattern is very useful. If you're interested check out how I used it in Fastify's type definitions for the different http/s/2 server options objects.
Check out my challenge solution playground
Intermediate/Advanced - Trunk or Treat
The Trunk or Treat challenge starts with an array of spots. It then has a type TrunkOrTreatResults
that has a lot of repetitive property definitions. The challenge tells us that it is already out of sync with the list of spots... so rather than more copy-pasting, what can we do to fix this type?
Consider what the type actually is representing, a map of the spots listed in trunkOrTreatSpots
. And in TypeScript there is a concept called mapped types. Often times, mapped types are used to transform one interface into another such as:
interface Person {
name: string;
age: number;
}
interface PersonPartial {
[P in keyof Person]?: Person[P];
}
But the trunkOrTreatSpots
is an array constant, how would one write a mapped type expression for a list of strings? Think back to the day 1 challenge; use [number]
to get the index type of the list. Then, using the typeof
keyword we can transform that index type returned from trunkOrTreatSpots[number]
into a union of the strings. And now with this we can pass it to a mapped type expression:
type TrunkOrTreatResults = {
[k in typeof trunkOrTreatSpots[number]]: {
done: boolean,
who: string,
loot: Record<string, any>
}
}
The new TrunkOrTreatResults
type now automatically includes any value that exists in trunkOrTreatSpots
. One important detail though, trunkOrTreatSpots
must be a constant (denoted by as const
). Since all of this needs to be static, TypeScript needs to be certain trunkOrTreatsSpots
won't change in order for your code to remain type safe.
Check out my challenge solution playground
Challenge 4
Try it for yourself here
Beginner/Learner - Lock it down with Readonly
TypeScript really has a utility type for everything. This challenge tasks us with making the two given types Rectory
and Room
readonly so that their properties cannot be modified. One solution is to go over each property and add the readonly
property, but as we learned yesterday, we can map one type into another fairly easily. Earlier in my post I showed how one may transform an interface from being all required properties, to all optional ones. This challenge requires us to transform these types into readonly versions.
type ReadonlyRectory = {
readonly [k in keyof Rectory]: Rectory[k]
}
type ReadonlyRoom = {
readonly [k in keyof Room]: Room[k]
}
There is alot of similarities between these two types, maybe with the help of a generic we can create a utility:
type Readonly<T> = {
readonly [k in keyof T]: T[k]
}
😉 And look at that, we just recreated the TypeScript utility Readonly
. Check it out in lib.es5.d.ts.
For my final solution I wrapped the given types with Readonly
:
type Rectory = Readonly<{
rooms: Room[]
noises: any[]
}>
type Room = Readonly<{
name: string
doors: number
windows: number
ghost?: any
}>
Check out my challenge solution playground
Intermediate/Advanced - Cutest Halloween Competition
Alright, this one definitelly took me a little bit. It was great to use the new template literal types, but I tried to rewrite the function using some functional programming aspects and didn't get very far. The start to my solution is by defining a new type ID
type ID = `${lowercase typeof breeds[number]}-${lowercase typeof costumes[number]}`
One thing I totally missed when I first read the 4.1 post was the lowercase
operator (and the corresonding uppercase
, capitalize
, and uncapitalize
ones too). The ID
type uses the same array index type thing I've used in the previous challenges. It concatenates the two resulting unions of lowercase strings together into a template string seperated by a -
.
Next, I modified the const winners
declaration to use a type cast.
const winners = {} as Record<ID, ReturnType<typeof decideWinner>>
I did it this way since winners
at the time of declaration isn't actually filled with values, but since the immediate following lines do that, its an okay type cast. Its for this exact reason that I tried rewriting the for loops so that the interpreter infers this record type automatically.
If you can figure that out let me know! Would love to check out your algorithm.
Finally, I added one more type cast to the id
declaration to complete my solution.
const id = `${breed}-${costume}`.toLowerCase() as ID
Check out my challenge solution playground
Challenge 5
Try it for yourself here
Beginner/Learner - Generic trick or treating
I really appreciated this challenge, it is a great demonstration for generics. Generics go hand-in-hand with DRY programming. In this challenge there are four houses. They all share the same object structure so we can first extrapolate the type into a shared House
type.
type House = {
doorNumber: number;
trickOrTreat(): string;
restock(items: string): void;
}
Now each house can be rewritten as an extension of this base type
type FirstHouse = House & {
doorNumber: 1,
trickOrTreat(): "book" | "candy";
restock(items: "book" | "candy"): void;
}
// ...
But we're still repeating another part of these types, the return value of trickOrTreat
and the argument of restock
. Using a generic, we can make the House
type even better.
type House<Items> = {
doorNumber: number;
trickOrTreat(): Items;
restock(items: Items): void;
}
Now, each house type can be defined as
type FirstHouse = House<"book" | "candy"> & {
doorNumber: 1;
};
// ...
The base House
type passes its generic down to both methods.
Check out my challenge solution playground
Intermediate/Advanced - Movie Night
// You're part of a team scheduling a movie night, but someone accidentally
// considered the movie "The Nightmare Before Christmas" to be a halloween
// movie, which it really isn't.
I'd like to point out that this statement is in fact false. The Nightmare Before Chistmas is one of the best Halloween and Christmas movies since it can be enjoyed equally on both holidays. 🎃
But anyways, back to the challenge at hand, and oh is this one a doozy. I got the first part fairly easily, but the second part (kids schedule) was something else.
My solution makes use of many things previously used in these challenges, mapped types, template literals, and utility types. To kick off my solution I extracted the type from moviesToShow
and defined a type for the schedule operations.
type Movies = typeof moviesToShow
type scheduleOps = 'getVHSFor' | 'makePopcornFor' | 'play'
Then using template literal types I create a movieNames
type which capitalizes the keys of Movies
.
type movieNames = `${capitalize keyof Movies}`
I combined this type with the scheduleOps
to create another ops
type. This ops
type is then used in the utility type Record
to define the Schedule
type.
type ops = `${scheduleOps}${movieNames}`
type Schedule = Record<ops, () => void>
Inside of makeScheduler
, I casted schedule
to the Schedule
type. Inside of the for loop, I casted the string operations to their corresponding types.
const schedule = {} as Schedule
// inside of the for loop
const capitalName = movie.charAt(0).toUpperCase() + movie.slice(1) as movieNames;
schedule[`getVHSFor${capitalName}` as ops] = () => {}
schedule[`makePopcornFor${capitalName}` as ops] = () => {}
schedule[`play${capitalName}` as ops] = () => {}
These template literal types are quite impressive!
The second part of the challenge requires doing something similar to the first, but this time filtering out any movies that is not forKids
. I immediately tried to use Extract
again but its not the right utility here. Instead I built a mapped-type based on the Movies
type I created at the beginning.
type kidsMovies = {
[K in keyof Movies]: Movies[K] extends { forKids: true } ? K : never
}[keyof Movies]
There is a lot of funny business going on here; lets break it down. First, the mapped expression is K in keyof Movies
. This creates a union of all of the movies in Movies
and assigns it to K
. The value is another expression, Movies[K] extends { forKids: true } ? K : never
. This expression checks if the type returned from Movies[K]
satisfies the forKids
truthiness. If it does, rather than returning that whole type, we just return the key K
(and otherwise never
). At the end of this type definition we have one last trick, we index using keyof Movies
. In short, this applies every key from the original type Movies
to the one just generated using mapped types. When one of these keys returns a never
value (the falsy case of the mapped type expression), the interpreter leaves that value out of the resulting union. Thus, we are left with a union type of just the kids movies.
With this new list of movies in hand, I recreate the ops and schedule types and use them in the makeKidScheduler
the same way I did previously
type KidsOps = `${scheduleOps}${capitalize kidsMovies}`
type KidsSchedule = Record<KidsOps, () => void>
Check out my challenge solution playground
Conclusion
First, huge thank you to the TypeScript team for putting these challenges together. Even as a seasoned TS developer, I had a blast solving each beginner and intermediate challenge. I'd like to specially promote Gabrielle Crevecoeur's finale post which has a great solution for the final challenge. Make sure you follow her on dev.to and twitter.
I understand my solutions might not be the best solutions, but thats the magic of programming. There is never just one way to do things. Furthermore, I'd like to preface that your solutions (and the ones posted by the TS team) may be quite similar (if not the same) as mine. Everything I've presented here is my own work, and any similarities are purely coincidental.
I encourage you to try them out on your own and discuss your solutions with me in the comments below.
Happy Halloween! 🎃👻🕷
Posted on November 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.