DEV Community

loading...
Cover image for Type | Treat The Finale
typescript

Type | Treat The Finale

gcrev93 profile image Gabrielle Crevecoeur ・5 min read

Thank you so much for participating in TypeScript's Type | Treat coding challenges! Unfortunately, we have come to the end of our spooky journey but no worries, there will be more challenges to come in the future!

Beginner/Learner Challenges

  1. Investigate haunting data from ghosts

  2. Plotting pumpkin types

  3. Tallying Trick or Treat responses

  4. Stop a sneaky spooky moving things around

  5. DRY the houses when re-stocking

Intermediate/Advanced Challenges

  1. Sort Trick or Treat loot

  2. Bust ghosts to guard Manhattan

  3. Track Trunk or Treat locations

  4. Help the annual halloween puppy parade

  5. Put on a horror movie marathon

Yesterday's Solution

Beginner/Learner Challenge

Like many challenges, you answer to this depends on how thorough you wanted to type the houses.
The in-challenge text tries to guide you to answer with a single generic type which passes the first argument to both trickOrTreat and restock.

type House<Candy> = {
  doorNumber: number
  trickOrTreat(): Candy;
  restock(items: Candy): void;
}

type FirstHouse = House<"book" | "candy">

type SecondHouse = House<"toothbrush" | "mints">

// ... same pattern for the rest
Enter fullscreen mode Exit fullscreen mode

This could be enough, and that's totally enough type safety for cases like this. This does lose the doorNumber being exact though. So, here are two different routes to give the doorNumber to each house:

// Via a 2nd generic argument
type House<DoorNumber, Candy> = {
  doorNumber: DoorNumber
  trickOrTreat(): Candy;
  restock(items: Candy): void;
}

type FirstHouse = House<1, "book" | "candy">

type SecondHouse = House<2, "toothbrush" | "mints">

// ... same pattern for the rest
Enter fullscreen mode Exit fullscreen mode

and

type House<Candy> = {
  doorNumber: number
  trickOrTreat(): Candy;
  restock(items: Candy): void;
}

// Via intersection types:
type FirstHouse = House<"book" | "candy"> & { doorNumber: 1 }

type SecondHouse = House<"toothbrush" | "mints"> & { doorNumber: 2 }
Enter fullscreen mode Exit fullscreen mode

Our answer.

Intermediate/Advanced Challenge

OK, this one is tricky. It's based on this Playground example.

We started out by making types for passing the data around

type Movies = typeof moviesToShow
type Movie = { forKids: boolean }

// Template strings literals to describe each task
type Get<T extends string> = `getVHSFor${capitalize T}`
type MakePopcorn<T extends string> = `makePopcornFor${capitalize T}`
type Play<T extends string> = `play${capitalize T }`

// A union of the above literal types
type Tasks<T extends string> = Get<T> | MakePopcorn<T> | Play<T>
Enter fullscreen mode Exit fullscreen mode

These gave us a set of primitives which could work together to create this whopper:

type MakeScheduler<Type> = {
  [Field in keyof Type as Tasks<Field extends string ? Field : never>]: () => void;
};
Enter fullscreen mode Exit fullscreen mode

This type uses the new as syntax for mapped types in TypeScript 4.1 to essentially map each field (Field) from the keys in the input type (Type) to the union Tasks above. This means that each field is converted into three templated literals:

input: `"halloween"` turns to:
  ├─ Get<"halloween"> -> `getVHSForHalloween`
  ├─ MakePopcorn<"halloween"> -> `makePopcornForHalloween`
  └─ Play<"halloween"> -> `playHalloween`
Enter fullscreen mode Exit fullscreen mode

Which is declared to be a function which returns void.

This type is then used as the return type for the makeScheduler function:

function makeScheduler(movies: Movies): MakeScheduler<Movies> {
Enter fullscreen mode Exit fullscreen mode

For simplicities sake, we skipped typing the inside of the function - though the folks who did that, good work!

The second part added one simple constraint, but one that requires some work to get right. We wanted to take into account whether a movie was for kids of not inside the type system.

Our answer for this was to recreate the scheduler function above, and to add the logic for removing those types during the type mapping process.

type MakeKidsScheduler<Type> = {
  [Field in keyof Type as Tasks<Field extends string ? Field : never>]:
    Type[Field] extends { forKids: true } ?  () => void : never;
};
Enter fullscreen mode Exit fullscreen mode

Instead of returning a () => void, we inserted a conditional type in the return position which first checked if forKids is true in the original type. If it was, then it returned the function - otherwise it returned never. Returning never here would mean that the function would not exist - removing them from the mapping process.

The community came up with quite a few alternative takes which provided type safety inside the functions and used different routes like removing the non-kids movie keys ahead of time.

Our answer

Share Your Experience

We would love to hear your feedback on this weeks challenges whether good or a bad! If you can please take our quick 4 question survey that can be found here

Want More?!

If you want to learn more about TypeScript, check out some of our best resources:

Happy Typing :)

Discussion (1)

pic
Editor guide
Collapse
jbojcic1 profile image
Josip Bojčić • Edited

Is there a way to make content of the makeScheduler function type safe in the advanced challenge? In the proposed solution you can put anything in the schedule object as it is set to any