Skip to content

Instantly share code, notes, and snippets.

@orta
Last active November 3, 2021 16:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save orta/927ccc66ae3022dc64c4f650109b937a to your computer and use it in GitHub Desktop.
Save orta/927ccc66ae3022dc64c4f650109b937a to your computer and use it in GitHub Desktop.
Type | Treat 2021

The Challenge

Welcome to TypeScript's second set of virtual code challenges: Type | Treat (or "Type or Treat")! We will be presenting some "spooky" code challenges that will allow you to get deeper into the TypeScript language in a fun way.

Are The Challenges For Experienced TypeScript Developers Only?

Nope! We want all developers, familiar with TypeScript or not to be apart of Type | Treat.

Beginner/Learner Challenge

Using the new music streaming service from the TypeScript team, Typify, set up and share your halloween playlist with your friends.

Head to this link and help us figure out to avoid passing typos.

Intermediate/Advanced Challenge

You've figured out your costume, but making it is a bit of a process. Can you figure out how to make all of the parts come together in a type-safe way?

Head over to start sorting.

// In prepration for halloween, you've become the shared DJ for
// all your friends. You've set up the system to distribute the music
// using the hip new typeify API/
import {typeifyAPI} from "type-or-treat"
const playlist = [
"The Legend of Sleepy Hollow by The Monotones.mp3",
"(It's a) Monster's Holiday by Buck Owens.mp3",
"Bo Meets the Monster by Bo Diddley.mp3",
"Purple People Eater Meets the Witch Doctor by The Big Bopper.mp3",
"Screamin Ball (at Dracula Hall) by The DuPonts.mp3",
"Batman, Wolfman, Frankenstein, or Dracula by The Diamonds.mp3",
"Frankenstein Twist by The Crystals.mp3",
"'Thriller' by Michael Jackson.mp3"
]
playlist
// ^?
// TypeScript thinks this is a list of strings, which is true - but can you
// make TypeScript treat the playlist array more "as constants"?
const api = typeifyAPI("my_api_key")
api.connect()
function playSong(song: string) {
api.play(song)
}
playSong("Purple People Eater Meets the Witch Doctor by The Big Bopper.mp3")
// This works great! But it could be a little more resistent to typos, can you
// think of a way to change the `playSong` `song` parameter reuse the "typeof" the
// playlist, so that TypeScript raises an error if there's a typo?
playSong("(It's a) Monster's Holiday by Buck Owens.mp3")
playSong("The Legend of Sleepy Hollow by The Monotones.mp3")
playSong("Batman, Wolfman, Frankenstein, or Darcula by The Diamonds.mp3")
// Costume prep time, you figure "why not dress up like
// the antagonists in the Squid Game?", it seems pretty easy
// based on your old Casa de Papel outfit.
// ( https://duckduckgo.com/?q=casa+de+papel&iax=images&ia=images)
// You set aside time to figure out what you're going to need to
// convert the constume and sketch out the potential work to
// give yourself an idea of timescale:
import {search, printer} from "type-or-treat"
const findOldCostume = () => {
const jumpSuit = search.attic() || search.closet() || search.garage()
const estimate = 30
return { jumpSuit, estimate }
}
const createNewMask = (costume: any) => {
const mask = printer.printMask()
mask.shape = "circle"
costume.estimate += 60
return { ...costume, mask }
}
const createBodyCamera = (costume: any) => {
const camera = printer.printCamera()
// It will need painting too
costume.estimate += 45
return { ...costume, camera }
}
const assembleCostume = () => {
return createBodyCamera(createNewMask(findOldCostume()))
}
// Great, this creates an assembly line of all the parts and
// the work you need to do. However, it relies on `any`s which
// makes working with the actual costume a bit ambiguous:
const costume = assembleCostume()
// ^?
// Can you think of a way to fix the inference without creating
// new `type`s or `interface`s, or replacing the anys with `{ ... }`?
// Ideally this typeof problem can be solved so that you can
// make a change in one of the functions and not need to update the types.
// You'll know it worked when this line raises an error
console.log(`It should take about ${costume.time} minutes`)
// Now that we've got a working pipeline, can you generate a type
// which represents the end-state of assembleCostume so that others
// can re-use it later?

Beginner/Learner Challenge

The first part of the solution for this challenge used as const to trigger "Literal Inference" - basically telling TypeScript "Don't convert the array to string[] but consider it a constant set of string literals. This meant that playlist[0] stopped returning string and started returning "The Legend of Sleepy Hollow by The Monotones.mp3".

  const playlist = [
      "The Legend of Sleepy Hollow by The Monotones.mp3",
      ...
- ]
+ ] as const

The second part of the challenge used typeof types to extract the type from the playlist array. Without the first change, this would be string but after the change this meant the full array of different types. You then needed to use the type index syntax [number] to declare that you want any potential string from that array.

- function playSong(song: string) {
+ function playSong(song: typeof playlist[number]) {
      api.play(song)
  }

Successfully completing this challenge would raise an error in the final code samples due to a subtle typo.

Our answer.

Intermediate/Advanced Challenge

This pattern is quite common in code we write in TypeScript codebases, you create one function which takes the result of another and keeps passing objects between functions in a pipeline. One of the best techniques for simplifying this design pattern is to use ReturnType with typeof myFunc to map the return type of one function to the paramter of another. This removes the need for intermediary types which need to be updated when the functions change.

  const findOldCostume = () => {
      // ...
      return { jumpSuit, estimate }
  }
  
- const createNewMask = (costume: any) => {
+ const createNewMask = (costume: ReturnType<typeof findOldCostume>) => {
      // ...
      return { ...costume, mask }
  }

The little extra step at the end was a small reminder that you can use this technique to provide a type which can be re-used everywhere.

type Costume = ReturnType<typeof assembleCostume>

Our Answer.

import {cookPumpkinSoup} from "type-or-treat"
// You're helping out at a local food-bank trying to sort a
// big pile of pumpkins into something which is usable for making
// Thai pumpkin soup (add some lemongrass, chilli and coconut milk.)
const pumpkins = [
{ color: "green", soundWhenHit: "dull thud" },
{ color: "purple", soundWhenHit: "echo-y" },
{ color: "green", soundWhenHit: "dull thud" },
{ color: "white", soundWhenHit: "squishy" },
{ color: "orange", soundWhenHit: "echo-y" },
{ color: "green", soundWhenHit: "dull thud" },
{ color: "orange", soundWhenHit: "echo-y" },
{ color: "blue", soundWhenHit: "echo-y" },
{ color: "orange", soundWhenHit: "echo-y" }
]
// Can you extract the types for three different categories of these pumpkins?
type UnderripePumpkin = {}
type RipePumpkin = {}
type OverripePumpkin = {}
// We'll use a type union to say that a pumpkin can be any of these possible types
type Pumpkin = UnderripePumpkin | RipePumpkin | OverripePumpkin
// Looks good, now lets make a pie, we want to first get out all of the ripe pumpkins.
// This code works at runtime, but TypeScript doesn't seem to understand that `pumpkinsForPie`
// is only RipePumpkin's now. Can you think of a way to change the `isRipe` to "guard" the "type"?
function isRipe(pumpkin: any) {
return "soundWhenHit" in pumpkin && pumpkin.soundWhenHit === "echo-y"
}
const pumpkinsForPie = pumpkins.filter(isRipe)
// ^?
const soup = cookPumpkinSoup(pumpkinsForPie)
// ^?
// Welcome back from your robotics hack camp! You've been asked if
// you can use your new skills to make a robot which handles
// mixing the punch for an upcoming party.
// Punch is an interesting drink because you can put all sorts of things
// into it, yet it is always classed as "punch" when you make a cup.
type Punch = {
flavour: string
ingredients: (string | { fruit: string, number: number })[]
}
// We've mapped out what will happen at your party, can you write a class
// which works with all the below code?
class PunchMixer {
// Your code here
}
// (if you want to try this without classes, you can drop the new below,
// but otherwise, please leave this code alone)
const mixer = new PunchMixer()
// Some actions which happen during the party, mainly used to validate
// your class works as expected
const enterTheParty = (name: string) => {}
const grabAGlass = (name: string, punch: Punch) => {}
const topUpPunch = (name: string, punch: Punch) => {}
const addIce = (punch: Punch) => {}
function partyStarts() {
addIce(mixer.punch)
enterTheParty("A")
mixer.punch = "Guiness 0.0"
enterTheParty("B")
mixer.punch = { fruit: "strawberries", number: 6 }
enterTheParty("C")
grabAGlass("B", mixer.punch)
topUpPunch("C", mixer.punch)
mixer.punch = { fruit: "grapes", number: 23 }
mixer.punch = "Pineapple Juice"
// (optional, if you run this code sample, this should not error)
if (mixer.punch.ingredients.length !== 4) {
console.error("Something got mixed up in the punch")
}
}
// Part 2 is a few lines below this
partyStarts()
/**
// Congrats, that worked out well (we hope!) so you'd like
// to scale your operation to handle many mixers in the party.
// To do that you'd like to make the class generically available
// and to have a function for vending out their classes - no need
// to worry too much about the implementation details here just
// get the types working as expected.
type SoloCup = { color: "red" }
type Glass = { size: "small" }
function partiesAtAnyScale() {
const livingRoomMixer = new PunchMixer<SoloCup>()
const kitchenMixer = new PunchMixer<Glass>()
enterTheParty("A")
kitchenMixer.punch = "Stryyk"
const glass = kitchenMixer.vend()
// ^?
glass.size
enterTheParty("B")
livingRoomMixer.punch = "Rhubarb Juice"
const cup = livingRoomMixer.vend()
// ^?
cup.color
}
*/

Beginner/Learner Challenge

There is many ways to decide how to type existing data, you could use literal types when you're sure of the exact formats - or be more liberal and use string when you expect a variety. We opted for literals, but using string is totally cool too.

type UnderripePumpkin = {
    color: "green",
    soundWhenHit: "dull thud"
}
type RipePumpkin = {
    color: "purple" | "orange" | "blue",
    soundWhenHit: "echo-y"
}
type OverripePumpkin = {
    color: "green" | "white",
    soundWhenHit: "squishy"
}

The second part of the challenge used type predicates (or type guards) annotates a function which returns a booleon with narrowing information about the paramters. This means we can tell TypeScript that when the return values to isRipeis true, then the argument pumpkin is of the type RipePumpkin:

- function isRipe(pumpkin: any) {
+ function isRipe(pumpkin: any): pumpkin is RipePumpkin {
       return "soundWhenHit" in pumpkin && pumpkin.soundWhenHit === "echo-y"
  }

Successfully completing this challenge would have no errors, and the type for `.

Our answer.

Intermediate/Advanced Challenge

This challenge was first about understanding different read vs write properties available in both classes and interface/typed objects. Personally, I've seen this with document.location a lot where you always get a rich object when you read but can write to that property with a string. We wanted a similar concept, but using punch which for me is generally a 'throw it all in and see what happens' style of drink.

class PunchMixer {
  #punch: Punch = {flavour: '', ingredients: []};

  public get punch(): Punch {
      return this.#punch;
  }

  public set punch(punch: Punch | Punch['ingredients'][number]) {
      if (typeof punch === 'string') {
          this.#punch.ingredients.push(punch);
      } else if ('flavour' in punch) {
          this.#punch = punch;
      } else {
          this.#punch.ingredients.push(punch);
      }
  }
}

This solution uses a mix of private class fields, indexed types and type narrowing to set up a local punch object which is always returned.

The next step was to make this class generic in some form so that a type parameter passed in to the class would dictate what the return value of a vend function was.

- class PunchMixer {
+ class PunchMixer<MixerType> {
+    mixer!: MixerType;

  // ...
+   public vend(): MixerType {
+        return this.mixer;
+    }
  }

We were not too worried about how you passed back the MixerType - our first draft had return {} as MixerType but a private field feels nicer.

Our Answer.

// You're working on your uber-for-toilet-papering-a-house startup
// and people keep giving you nonsense lengths for how much toilet
// paper they are going to need. You initially gave the input a
// lot of flexibility, but felt like maybe `string` is a bit too
// much freedom.
// You've heard of template literals, maybe they could be useful?
type Length = string
// Your API function, we'll be using the input to this function
// to validate if your type is right or not
function req(input: Length) {
// Remove NaN, minus values,
// Do some work
}
// To get started, here's some examples which should always file
const shouldFail = () => {
req("")
req("apx")
req("rem")
req("abc")
}
// Start here, you Length to be able to check for a number and an "in" prefix
const simpleInputs = () => {
req("0in")
req("12in")
}
// What about more than one prefix for the folks who think in cm?
const extraUnits = () => {
req("1.5cm")
req("20cm")
}
// It feels right that if you pass "0", you should be able to go unit-less
const handleZero = () => {
req("0")
}
// What about allowing whitespace between the number and the unit?
const handleWhitespace = () => {
req("12 cm")
req("14 in")
}
// If you have all of the above passing, congrats! That's more than enough
// to have completed the challenge. This challenge has a secret *experts* section
// for people who really want to test themselves.
const thingsWhichComplicateTheMatter = () => {
// This is an allowed number using bigint notation, it's allowed, just
// the annotation is unique, ideally you catch big numbers in the JS
req(`${0.3e21}cm`)
// Minus numbers don't make sense in our case, can you ensure that
// it only accepts positive numbers?
req("-12 cm")
// It is possible to raise an error with these two, can you
// figure out how? We know of two implementations, one simpler with
// an interesting trade-off, and another which accurately can catch these
// cases without that trade-off.
req(`${Infinity}cm`)
req(`${NaN}cm`)
}
// You've set up a little poster making business, and for halloween
// you're doing specials when people have halloween parties. The best
// feature is that you make the titles extra spooky!
// You noticed that code which you thought would work is raising an
// error from TypeScript down at the `addTadaEmoji`. It seems that `makeTitle`
// isn't keeping the type is loosing the string literal when it comes in -
// can you make it pass by changing the definition of `makeTitle`?
function makeTitle(str: string) {
return "<spooky>" + str.toUpperCase() + "</spooky>"
}
const requiresTadaEmoji = (str: string) => { return str === "<spooky>PARTY</spooky>"}
const addTadaEmoji = (str: "<spooky>PARTY</spooky>") => `:tada:${str}:party:`
const addToPoster = (str: string) => { }
const setupHeader = () => {
const title = makeTitle("party")
if (requiresTadaEmoji(title)) {
addTadaEmoji(title)
}
addToPoster(title)
}
// Let's be honest, this system you've set up is a little janky
// because it has to interact with a machine learning program. The
// program gives you back a comma-separated string for the footer
// information. You convert this into a footer but find yourself
// losing type information when you use this function:
function setupFooter(str: string) {
// validate string etc
return {
name: str.split(",")[0],
date: str.split(",")[1],
address: str.split(",")[2]
}
}
// Ideally so that you get the right types below
const footer = setupFooter("Danger McShane,Halloween 2021,The Internet")
footer.name
// ^?
footer.date
// ^?
footer.address
// ^?

Beginner/Learner Challenge

I wonder if we over-indexed on the difficulty here, and we're interested if you dropped off somewhere through this task because we had less submissions than usual for this challenge. The goal was to have you build out a template string literal type which accounted for string input which roughly matched how CSS's stringy variables worked.

You started with:

type Length = string

Which accepts all possible strings, next we show some examples which should always fail. The key one here being that an empty string should fail: "". Next we provided some valid input for you to work with:

type Length = `${number}in`

// Works with:
req("0in")
req("12in")

Giving you a sense that a number can be used in the template slot - which allows for all sorts of possibilities.

Next we gave samples with different prefixes, so "in" and "cm" would need to be handled. To get that right, you would need to use a union:

type Unit = "cm" | "in"
type Length = `${number}${Unit}`

// Works with:
req("0in")
req("12in")
req("1.5cm")
req("20cm")

Next we threw a curve ball - "0" should also be acceptable, this is a bit of a curve ball, but also it's a bit of a trick:

type Unit = "cm" | "in" | ""
type Length = `${number}${Unit}`

// Works with:
req("0in")
req("12in")
req("1.5cm")
req("20cm")
req("0")

The lack of a unit is just an empty string unit! Only one more thing now, and that is allowing a space inbetween the number and unit. This could be done via another type also:

type Unit = "cm" | "in" | ""
type Space = " " | ""
type Length = `${number}${Space}${Unit}`

// Works with:
req("0in")
req("12in")
req("1.5cm")
req("20cm")
req("0")
req("12 cm")
req("14 in")

That was is for the easy parts of the challenge. It's pretty tricky, because it requires that you understand that number can be anything in the template string and to understand how a union can allow for different types of strings inside the type. That's all in the main docs, but it could be a lot of ideas to learn at once.

This challenge also had a set of complications, cases where the version of the the Length type we expected people to build would provide interesting edge cases:

req(`${0.3e21}cm`)
req("-12 cm")
req(`${Infinity}cm`)
req(`${NaN}cm`)
Click to learn about these cases
req(`${0.3e21}cm`)

Acted as a potential clue to an alternative answer for these failing cases:

req(`${Infinity}cm`)
req(`${NaN}cm`)

Because number can be switched out with bigint in the type of Length:

- type Length = `${number}${Space}${Unit}`
+ type Length = `${bigint}${Space}${Unit}`

This meant you couldn't pass in Infinity or NaN but also broke req("1.5cm") because you can't have point values. This could be fixed via:

type Length = `${bigint}${Space}${Unit}` | `${bigint}.${bigint}${Space}${Unit}`

Which describes both possibile cases with a "." and without. This technique still doesn't handle the req("-12 cm"), and actually, it introduces a whole new problem: req("-12.-12cm") is allowed!

We spotted a good answer from @danvdk which revolved around using string manipulation instead, by introducing a Digit type:

type Whitespace = '' | ' ';
type Unit = 'in' | 'cm';
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type Length = `${Digit}${number | ''}${Whitespace}${Unit}` | '0';

This solution correctly handles the case of req("-12 cm") but via that number would allow something like req("1-22 cm") - which you can pretend is to handle an input range. It wouldn't be hard to take this solution and reasonably cover additional edge cases. Very cool solution.

Our answer

Intermediate/Advanced Challenge

The intermediate challenge was on type literals mixed with generics functions. The challenge started with this function:

function makeTitle(str: string) {
    return "<spooky>" + str.toUpperCase() + "</spooky>"
}

The goal was to keep track of string literals through this function. To do this, you need to switch the str: string to be a type argument:

function makeTitle<Str>(str: Str) {
    return "<spooky>" + str.toUpperCase() + "</spooky>"
}

You know that the type argument has to be a string, which you can tell TypeScript via <Str extends string>, then you can re-use the Str in the return position:

function makeTitle<Str extends string>(str: Str): `<spooky>${Uppercase<Str>}</spooky>` {
    return "<spooky>" + str.toUpperCase() + "</spooky>"
}

You'd think this would be it, but str.toUpperCase actually converts the str to a string! Tricky, you'd need to think creatively here and you have three options:

  1. Use an as because you know better than the compiler:

    function makeTitle<Str extends string>(str: Str): `<spooky>${Uppercase<Str>}</spooky>` {
        const shouty = str.toUpperCase() as Uppercase<Str>
        return `<spooky>${shouty}</spooky>`
    }
  2. Override toUpperCase to support template literals:

    interface String {
        toUpperCase<T extends string>(this: T) : Uppercase<T>
    }
  3. Or create a new function which supports template literals.

This would take the "party" used on line 19 and convert it to "<spooky>PARTY</spooky>". This would remove the compiler error on addTadaEmoji.

The second part was about re-using the type parameters inside argument for the function. The challenge started with:

function setupFooter(str: string) {
    // validate string etc
    return { 
        name: str.split(",")[0],
        date: str.split(",")[1],
        address: str.split(",")[2]
    }
}

Would lose string literals passed in as str. You knew ahead of time that there were three separate parts of information you were interested in:

function setupFooter<Name extends string, Date extends string, Address extends string>(str: string) {

These could then be used inside the replacement for string:

function setupFooter<Name extends string, Date extends string, Address extends string>(str: `${Name},${Date},${Address}`) {

Which would correctly set up these variables for re-use later:

function setupFooter<Name extends string, Date extends string, Address extends string>(str: `${Name},${Date},${Address}`) {
    // validate string etc
    return { 
        name: str.split(",")[0] as Name,
        date: str.split(",")[1] as Date,
        address: str.split(",")[2] as Address
    }
}

Successfully completing this challenge would show that name, date and address were not string but the strings passed in.

Our answer.

// You've got yourself bunch of bowls of candy, but the bowls are
// just "generic" bowls. You can put anything in them, and get anything
// back out of them.
function getBowl(items: any) {
return { items }
}
const chewyMixBowl = getBowl({ colors: ["red", "green", "blue"], strawberries: "some" })
// ^?
const candiedApplesBowl = getBowl({ types: ["Tanghulu", "Maçã do amor", "Toffee Apples"] })
// ^?
// This is a shame, because ideally this would raise a compiler arror:
candiedApplesBowl.items.type[0]
// Can you think of a way to let the parameter of `getBowl` keep the type
// of `items` through the function?
// Spoilers below as a part of the extension of this example
// Still a bit further for those on large screens
// Cool, there it is
// You've got yourself bunch of bowls of candy, but the bowls are
// just "generic" bowls. You grab a few fancy branded :tm: bowls and
// fill the bowls with their candy.
function fillBowl<T>(candy: T) {
return { candy }
}
const marsBowl = fillBowl("mars")
const snickersBowl = fillBowl("snickers")
const skittlesBowl = fillBowl("skittles")
// It turns out that a few kids are picky though,
// and only want to get one type of candy from each bowl.
const giveOutSnickers = (str: "snickers") => { }
const giveOutSkittles = (str: "skittles") => { }
const giveOutMarsBars = (str: "mars") => { }
// This means you will need to extend the `fillBowl` to
// handle passing a specific string literal through the
// the function. You will only need to add two words to
// the function to make this work.
giveOutSnickers(snickersBowl.candy)
giveOutSkittles(skittlesBowl.candy)
giveOutMarsBars(marsBowl.candy)
// You're in charge of running pumpkin rating competitions, you have
// a few to run and you've just got the size competition working.
interface Competitor {
/** In lbs */
weight: number;
}
interface SizeCompetition extends Competitor { }
interface DecorativeCompetition extends Competitor {
theme: string
}
// Your code works great when you were only handling competitions
// about the size of a pumpkin but when you started to handle cases like
// the decoration competitions - you found that that you didn't have
// the ability to access some of the extra fields found on the
// DecorativeCompetition! Can you extend the `check` function below
// to remove the compiler error below?
const check = (data: Competitor[]) => {
return data.map(competitor => {
if (competitor.weight > 2121.5) throw new Error("Stop the show, world record hit!")
return { ...competitor, judge: (...args: unknown[]) => { } }
})
}
const sizeCompetitors = [{ weight: 219 }, { weight: 120 }, { weight: 1376 }, { weight: 23 }]
const decorativeCompetitors = [{ theme: "movies", weight: 63 }, { theme: "portrait", weight: 44 }]
// The size competition works great
const sizeCompetition = check(sizeCompetitors)
// ^?
sizeCompetition.forEach(competitor => {
competitor.judge(competitor.weight)
})
// The decorative competition isn't passing the type-check though
const decorativeCompetition = check(decorativeCompetitors)
// ^?
decorativeCompetition.forEach(competitor => {
competitor.judge(competitor.weight, competitor.theme)
})

Beginner/Learner Challenge

This challenge aimed to be generics 101, first introducing the concept of making your function pass a type from the function to the argument:

-  function getBowl(items: any) {
+  function getBowl<T>(items: T) {
      return { items }
  }

The any acted as hint clue about where to look, and this example is almost the first code sample on the Generics chapter in the Handbook, so it felt like a good intro.

The second part involved understanding generic constraints, these are essential tools in helping you define the baselines for types which can be used in your function. In this case we didn't provide the word "constraints" but opted for a more cryptic clue by setting up the function most of the way, then saying you only needed two words:

-  function fillBowl<T>(candy: T) {
+  function fillBowl<T extends string>(candy: T) {
      return { candy }
  }

By saying that T extended string then the string literals are correctly passed through the function - which removes all the compiler errors.

Our answer

Intermediate/Advanced Challenge

The intermediate challenge also invovled generic constraints, so if you had just finished the beginner's then you were in a good place to figure this challenge. The key is to make a

- const check = (data: Competitor[]) => {
+ const check = <Type extends Competitor> (data: Type[]) => {
      return data.map(competitor => {
          if (competitor.weight > 2121.5) throw new Error("Stop the show, world record hit!")
          return { ...competitor, judge: (...args: unknown[]) => { } }
      })
  }

This is testing a few different things:

  • Writing generics wit han arrow function
  • Using an extends constraint for the interface subtypes
  • Re-using the type parameter inside the array

We left a tricky problem with this challenge, but explicitly didn't call it out. The function judge: (...args: unknown[]) is a types gap. There is no validating that the judge function actually works like expected. There are two approaches for handling this:

- return { ...competitor, judge: (...args: unknown[]) => { } }
+ return { ...competitor, judge: (...args: Array<T[keyof T]>) => { } }

This version from @faridz974 would ensure that the right values were used in the function (e.g. you couldn't accidentally put in an object to something which could only accept string and numbers) but it ignored the order. An accurate, but whole-heartedly not recommended for production version which does take order into account comes from converting an interface to a tuple on GitHub which is a bit too long to print in here, but here's a working implementation in the playground.

Our answer.

// For the Halloween weekend, you've decided to change the
// set of colors for your your website to be spooky themed
// While you're here, you may as well act like a good scout
// and improve the codebase while you're here:
const scheme = {
background: '#242424',
textColor: '#ffa52d',
highlightOne: '#cafe37',
highlightTwo: '#9e20ff'
} as const
// It looks like your type which represents all possible
// colors is out of date, can you change this type to
// be created directly from the keys of `scheme` above?
type SchemeNames = "background" | "textColor" | "highlightOne"
// It looks like there's this useful function `possibleSchemeItems`
// which would return the names of the colours but it returns a
// strings array. Can you improve this?
function possibleSchemeItems(colors: any): string[] {
const keys = Object.keys(colors) as string[]
return keys
}
// You spotted another case of an outdated string union, can
// you clean this up to by looking at the type of schema and
// then indexing ot the possible schema names?
type PossibleColors = '#242424' | '#ffa52d' | '#cafe37'
// Now that you've cleaned up these types, you want to make
// sure that there's a copy of the original scheme still around.
// Can you make sure that the `previousScheme` below still has
// the same keys as the `scheme` up above?
type Scheme = Record<string, string>;
const previousScheme: Scheme = {
background: '#111111',
textColor: '#c60800',
highlightOne: '#006ba1',
};
// You're running a bookshop, and during Halloween you
// have a special for horror books based on how many ghosts
// there are in the book.
interface Book { genre: string, applyDiscount: (discount: number) => void };
// For simplicity, we'll assume there are only two genres, but
// ideally we'd like to add as many as possible later down the line.
interface HorrorBook extends Book { genre: "horror", ghostCount: number, victimsCount: number }
interface RomanceBook extends Book { genre: "romance", protagonistName: string }
type Books = HorrorBook | RomanceBook;
// You want to handle the sale using an event-system, which looks like 'on[genre] => (book)'.
// To make your life easier, you have a map of the events to their books to their event name:
type IncomingBookMap = {
onhorror: HorrorBook;
onromance: RomanceBook;
}
// Your event system looks like this, but when you start to access the additional fields
// on each genre TypeScript starts to give errors. That's not great:
handleSale({
onhorror(book) {
book.applyDiscount(book.ghostcount * 5)
},
onromance(book) {
console.error(`No romance books discount during Halloween, even though ${book.protagonistName} is great`)
},
onother(book) {
// NOOP
}
})
// Here's the function where you'll be doing the work, can you convert this function to handle
// passing through the correct type to the correct function. We're going to try and work on it incrementally, so
// don't feel like you need to jump to a perfect answer yet.
function handleSale(events: Record<string, (e: Books) => void>) {
// We're ignoring the implementation details for this function.
// Meaning, the only possible work you need to do is in the `events` param above.
}
/// Step 1
// Can you make `handleSale` function only accept `onhorror` and `onromance` as keys?
/// Step 2
// Now that `onother` is correctly raising an error, you can remove it.
// On that subject, can you make `handleSale` accept one of the real function being absent? For example, right now
// you have to include `onromance` but it does nothing. To pull this off you're going to need to
// make some pretty fundamental changes to the type of `events` as you'll need a mapping modifier.
// Tip: Don't use `Partial` for this.
/// Step 3
// Now that the functions are set up, it's time to hit the main problem: the types
// for the `book` inside each function needs to be based on the name of the event.
// Can you pass that information through the type?
/// Bonus: Step 4
// Can we remove the need for `IncomingBookMap` all together, relying solely on `Books` to create the types?
// You'll need to use some template string manipulation to wrap up the entire function.

Giving Feedback

Sharing Your Answers

Once you feel you have completed a challenge, you will need to select the Share button in the playground code editor. This will automatically copy a playground URL of the current code to your clipboard.

Then either:

  • Go to Twitter, and create a tweet about the challenge, add the link to your code and mention the @TypeScript Twitter account with the hashtag #TypeOrTreat.

  • Leave us a comment with your feedback on the dev.to post, or in this post.

If you'd like to post both of your challenges, you'll need to include two links.

Feedback

We'd love your feedback on how the Type | Treats is going, we have a 6 question survey which will help us improve.

@cattode
Copy link

cattode commented Oct 30, 2021

Thanks for these great code challenges, it's super fun! 🤓
There seems to be a few typos in the following files:

- // To get started, here's some examples which should always file
+ // To get started, here's some examples which should always fail
- - Writing generics wit han arrow function
+ - Writing generics with an arrow function
-     book.applyDiscount(book.ghostcount * 5)
+     book.applyDiscount(book.ghostCount * 5)

(This gist seemed a good place to report them, but let me know if there's a better way!)

@orta
Copy link
Author

orta commented Nov 3, 2021

thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment