DEV Community

loading...

TypeScript - Type | Treat Halloween Challenge

ethanarrowood profile image Ethan Arrowood ・13 min read

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
Enter fullscreen mode Exit fullscreen mode

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 from Type all union members that are assignable to Union.

type AllCandies = Extract<ResultsFromHalloween, {candy: true}>
type AllTricks = Extract<ResultsFromHalloween, {trick: true}>
Enter fullscreen mode Exit fullscreen mode

Under the hood, the Extract type utilizes conditional types:

type Extract<T, U> = T extends U ? T : never
Enter fullscreen mode Exit fullscreen mode

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}>
Enter fullscreen mode Exit fullscreen mode

Yet again, TypeScript has a utility type for this operation.

Exclude<Type, ExcludedUnion> constructs a type by excluding from Type all union members that are assignable to ExcludedUnion.

This one also uses conditional types:

type Exclude<T, EU> = T extends EU ? never : T
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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[] {}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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',
}
Enter fullscreen mode Exit fullscreen mode

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',
}
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

😉 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
}>
Enter fullscreen mode Exit fullscreen mode

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]}`
Enter fullscreen mode Exit fullscreen mode

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>>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
// ...
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Now, each house type can be defined as

type FirstHouse = House<"book" | "candy"> & {
  doorNumber: 1;
};
// ...
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Then using template literal types I create a movieNames type which capitalizes the keys of Movies.

type movieNames = `${capitalize keyof Movies}`
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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] = () => {}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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! 🎃👻🕷

Discussion (2)

pic
Editor guide
Collapse
orta profile image
Orta

This is a great post, yeah all these trade-offs in the types make total sense too. Nice write-up!

Collapse
ethanarrowood profile image
Ethan Arrowood Author

Thank you! I really enjoyed these challenges.