Skip to content

Instantly share code, notes, and snippets.

@orta
Last active March 14, 2023 01:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save orta/303ebff59a6fc37f88c86e86dbdeb0e8 to your computer and use it in GitHub Desktop.
Save orta/303ebff59a6fc37f88c86e86dbdeb0e8 to your computer and use it in GitHub Desktop.
Type | Treats 2020

The Challenge

Welcome to TypeScript's first ever 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 but in a fun way.

Starting tomorrow, a new code challenge will be posted every weekday, along with its solution the day after. The last solution will be posted on Halloween day.

Are The Challenges For TypeScript Developers Only?

Absolutely not! We want all developers, familiar with TypeScript or not to be apart of Type | Treat. Every day will have two different types of challenges, one for beginners/learners and one for intermediate/advanced developers. That way everyone can participate.

Beginner/Learner Challenge

The TypeScript team is being hired to investigate recent hauntings in the community, and we are trying to figure which ghost is the trouble-maker! We found an API that will allow us to get data on the ghosts however, out code isn't fully optimized. So we need your help.

Head to this link and help us figure out the best type to use in one of our function parameters!

Intermediate/Advanced Challenge

Your kids have come back from trick or treating with a lot of loot. Someone's going to have to sort this pile, and it looks like that job has fallen to you. Can you conditionally filter the pile into manageable lists?

Head over to start sorting.

// You're making an app which connects to a database
// of local ghosts and their favourite haunts.
// You are creating the client which connects to that database,
// and you have an API response generated for you automatically.
// When you start trying to display the hauntings, you realize
// that you don't know how to type the parameter and just use `any`.
// `any` has its place, but a colleague recommended that you can
// use 'Indexed Types' to remove the 'any' without changing the
// GhostAPIResponse type.
type GhostAPIResponse = {
name: string
birthDate?: string
deathDate?: string
bio: string
hauntings: Array<{ title: string, provenance: string, location: string }>
}
const displayGhost = (ghost: GhostAPIResponse) => {
console.log(`Name: ${ghost.name}`)
if (ghost.birthDate) console.log(`Birthday: ${ghost.birthDate}`)
if (ghost.deathDate) console.log(`Died: ${ghost.deathDate}`)
console.log(`\nBio: ${ghost.bio}`)
console.log(`\nArtworks:`)
ghost.hauntings.forEach(artwork => {
displayHauntings(artwork)
})
}
// Your goal: remove this any, without changing GhostAPIResponse
const displayHauntings = (haunting: any) => {
console.log(` - Title: ${haunting.title}`)
console.log(` ${haunting.location}`)
console.log(` ${haunting.provemance}`)
}
// Your kids have returned home with a whole bag
// full of halloween loot, and you've taken the time to
// make a description of all of them:
type SnackBars = {
name: "Short Chocolate Bars"
amount: 4
candy: true
}
type Gumballs = {
name: "Gooey Gumballs"
color: "green" | "purples"
candy: true
}
type Apples = {
name: "Apples"
candy: true
}
type Cookies = {
name: "Cookies"
candy: true
peanuts: true
}
type SnickersBar = {
name: "Snickers Bar"
candy: true
peanuts: true
}
type Toothpaste = {
name: "Toothpaste"
minty: true
trick: true
}
type Pencil = {
name: "Pencil"
trick: true
}
// You create a single pile of all the results, and want to use
// this to share out the winnings among your kids.
type ResultsFromHalloween = SnackBars | Gumballs | Apples | SnickersBar | Cookies | Toothpaste | Pencil
// You're first going to need to separate out the candy from the treats,
// you can do that via conditional types.
// type AllCandies = ...
// type AllTricks = ...
// Almost there, but little 'Bobby Tables' cannot have peanuts. Can
// you make a list of candies just for him?
// type AllCandiesWithoutPeanuts = ...

Solution for Day 1

Beginner/Learner Challenge

The solution for this challenge used indexed types to extract a part of an existing type to avoid duplication. The need to use [number] is an interesting twist, because [0] or [1] (or any number) would have worked just as well too. Doing this right would also have raised the typo in the original code.

// Your goal: remove this any, without changing GhostAPIResponse
- const displayHauntings = (haunting: any) => {
+ const displayHauntings = (haunting: GhostAPIResponse["hauntings"][number]) => {
  console.log(` - Title: ${haunting.title}`)

Link

Intermediate/Advanced Challenge

// You're first going to need to separate out the candy from the treats,
// you can do that via conditional types.

- // type AllCandies = ...
+ type IsCandy<A> = A extends { candy: true } ? A : never;
+ type AllCandies = IsCandy<ResultsFromHalloween>

- // type AllTricks = ...
+ type IsTrick<A> = A extends { trick: true } ? A : never;
+ type AllTricks = IsTrick<ResultsFromHalloween>

// Almost there, but little 'Bobby Tables' cannot have peanuts. Can
// you make a list of candies just for him?

- // type AllCandiesWithoutPeanuts = ...
type HasPeanuts<A> = A extends { peanuts: true } ? A : never;
type AllCandiesWithoutPeanuts = HasPeanuts<AllCandies>

Our original answer relied on using Conditional Types to narrow the union, however we got a lot of responses using the Exclude utility type to make it a single liner:

type AllCandies = Exclude<ResultsFromHalloween, { candy: true }>

Which is a great solution. Full link

The Challenge

Beginner/Learner Challenge

Lets take a trip to the pumpkin patch and try to find the perfect one for our Jack O'Lantern. But in order to make sure we have located the right type of pumpkin, we need your help in identifying pumpkin types.

We created some starter code you can find here, lets see if you can finish it.

Intermediate/Advanced Challenge

Your job busting ghosts just got real tricky. Before you head in to guard Manhattan, you need to assert to those ghosts who is boss. Help finalize the ghost-busting algorithm because who else are you gonna call?

Need Extra Help?

If you need additional help you can utilize the following:

Happy Typing :)

// You've been keeping a diary of your trick or treat
// results for your street, they generally fall into
// three categories: treats, tricks and no-shows.
// Can you make three types which can describe of
// these results?
const treats = [
{ location: "House 1", result: "treat", treat: { candy: "Lion Bar" } },
{ location: "House 3", result: "treat", treat: { candy: "Mars Bar" } },
{ location: "House 4", result: "treat", treat: { baked: "Cookies", candy: "Reese's" } }
]
const tricks = [
{ location: "House 2", result: "trick", trick: "Magic" },
{ location: "House 7", result: "trick", trick: "Surprised" }
]
const noShows = [
{ location: "House 6", result: "no-show" }
]
// Now that you have the types, can you make them
// not duplicate 'location' and 'result' by using
// a union for 'result' and a new intersection type?
// Dang, COVID19 has really put a bind on the nature of trick or treating.
// Your block has opt-ed to instead do a trunk or treat instead.
// In a rush to prepare for the event, you hardcoded the
// results into the 'TrunkOrTreatResults' type which is already
// out of date - it's missing a few properties!
// Can you rewrite 'TrunkOrTreatResults' as an object type that
// stays in sync with the strings in 'trunkOrTreatSpots'?
const trunkOrTreatSpots = [
'The Park',
'House #1',
'House #2',
'Corner Shop',
'Place of Worship',
] as const
type TrunkOrTreatResults = {
"The Park": {
done: boolean,
who: string,
loot: Record<string, any>
},
"House #1" : {
done: boolean,
who: string,
loot: Record<string, any>
},
"House #2": {
done: boolean,
who: string,
loot: Record<string, any>
}
}
function makeTODO(spots: typeof trunkOrTreatSpots): TrunkOrTreatResults {
return spots.reduce((prev, current) => {
return {
...prev,
[current]: {
done: false,
loot: {},
who: ""
}
}
}, {} as TrunkOrTreatResults)
}
// You can preview the results via "Run" above
const todo = makeTODO(trunkOrTreatSpots)
console.log(todo)
// Works
todo["The Park"].done = true
// Should Work
todo["Corner Shop"].loot = {}
// Fails
todo["House #3"].done = false
// Spoiler-ish optional tips are a few lines below this
// Warning: Spoilers
// 1. This is a mapped types problem
// 2. There is a constraint on the mapped type argument
// 3. You can re-use a part of the technique from the beginner challenge 1 to get the exact keys from
// the array: https://dev.to/typescript/type-treat-challenge-2-3n16

Solution

Beginner/Learner Challenge

We were looking for some pretty simple types in this challenge:

type Treat = { 
  location: string
  result: "treat",
  treat: { candy: string, baked?: string } 
}

type Trick = {
  location: string
  result: "trick",
  trick: string
}

type NoShow = {
  location: string
 result: "no-show"
}

We can safely and efficiently reuse the properties in each field by extending them with an interface.

type House = {
  location: string
  result: "treat" | "trick" | "no-show"
}

interface Treat extends House { treat: { candy: string, baked?: string } }
interface Trick extends House  { trick: string }
interface NoShow extends House { trick: string }

We can specialize each result property with the specific value a Trick, Treat, or NoShow will hold by re-declaring it.

interface Treat extends House { result: "treat", treat: { candy: string, baked?: string } }
interface Trick extends House { result: "trick", trick: string }
interface NoShow extends House { result: "no-show" }

Which would give the exact string value for result in each type.

Our answer.

Intermediate/Advanced Challenge

This one either stumped people for ten minutes or was considered a breeze. We had a few different answers in the TypeScript team, but we think this one is the most elegant.

type Result = {
    done: boolean,
    who: string,
    loot: Record<string, any>
}

type TrunkOrTreatResults = Record<typeof trunkOrTreatSpots[number], Result>

This challenge was a great reminder that a Record is simply a type-alias to a conditional type:

type Record<K extends string | number | symbol, T> = { [P in K]: T; }

Which can convert the original answer:

type Result = {
    done: boolean,
    who: string,
    loot: Record<string, any>
}

type ResultMapper<P extends readonly string[]> = {
    [K in P[number]]: Result
};

type TrunkOrTreatResults = ResultMapper<typeof trunkOrTreatSpots>;

into code which is both easier to read, and requires knowing less language concepts. Kudos to the folks who made it seem very easy!

Here's our answer. Bonus: the weirdest answer.

The Challenge

Beginner/Learner Challenge

Don't freak out... Ok freak out a little bit cause WE ARE BEING HAUNTED! And they are after our code! We have tried everything but we cant seem to get it right!

Somehow they keep manipulating the objects set this snippet of code. Take a look and see if you can force the ghosts to stop moving things around.

Intermediate/Advanced Challenge

You've been roped into helping out the halloween puppy parade, it was such a distracting even that you did the minimum possible to spend as much time watching the show.

Now it's over, you figure it's time to clean up your code and try get it type-safe.

Need Extra Help?

If you need additional help you can utilize the following:

Happy Typing :)

// You've been keeping a diary of your trick or treat
// results for your street, they generally fall into
// three categories: treats, tricks and no-shows.
// Can you make three types which can describe of
// these results?
const treats = [
{ location: "House 1", result: "treat", treat: { candy: "Lion Bar" } },
{ location: "House 3", result: "treat", treat: { candy: "Mars Bar" } },
{ location: "House 4", result: "treat", treat: { baked: "Cookies", candy: "Reese's" } }
]
const tricks = [
{ location: "House 2", result: "trick", trick: "Magic" },
{ location: "House 7", result: "trick", trick: "Surprised" }
]
const noShows = [
{ location: "House 6", result: "no-show" }
]
// Now that you have the types, can you make them
// not duplicate 'location' and 'result' by using
// a union for 'result' and a new intersection type?
// Dang, COVID19 has really put a bind on the nature of trick or treating.
// Your block has opt-ed to instead do a trunk or treat instead.
// In a rush to prepare for the event, you hardcoded the
// results into the 'TrunkOrTreatResults' type which is already
// out of date - it's missing a few properties!
// Can you rewrite 'TrunkOrTreatResults' to an object type that
// stays in sync with the strings in 'trunkOrTreatSpots'?
const trunkOrTreatSpots = [
'The Park',
'House #1',
'House #2',
'Corner Shop',
'Place of Worship',
] as const
type TrunkOrTreatResults = {
"The Park": {
done: boolean,
who: string,
loot: Record<string, any>
},
"House #1" : {
done: boolean,
who: string,
loot: Record<string, any>
},
"House #2": {
done: boolean,
who: string,
loot: Record<string, any>
}
}
function makeTODO(spots: typeof trunkOrTreatSpots): TrunkOrTreatResults {
return spots.reduce((prev, current) => {
return {
...prev,
[current]: {
done: false,
loot: {},
who: ""
}
}
}, {} as TrunkOrTreatResults)
}
// You can preview the results via "Run" above
const todo = makeTODO(trunkOrTreatSpots)
console.log(todo)
// Works
todo["The Park"].done = true
// Should Work
todo["Corner Shop"].loot = {}
// Fails
todo["House #3"].done = false
// Spoiler-ish optional tips are a few lines below this
// Warning: Spoilers
// 1. This is a mapped types problem
// 2. There is a constraint on the mapped type argument
// 3. You can re-use a part of the technique from the beginner challenge 1 to get the exact keys from
// the array: https://dev.to/typescript/type-treat-challenge-2-3n16

Day 3's solutions

Beginner/Learner Challenge

We were looking for some pretty simple types in this challenge:

type Treat = { 
  location: string
  result: "treat",
  treat: { candy: string, baked?: string } 
}

type Trick = {
  location: string
  result: "trick",
  trick: string
}

type NoShow = {
  location: string
 result: "no-show"
}

We can safely and efficiently reuse the properties in each field by extending them with an interface.

type House = {
  location: string
  result: "treat" | "trick" | "no-show"
}

interface Treat extends House { treat: { candy: string, baked?: string } }
interface Trick extends House  { trick: string }
interface NoShow extends House { trick: string }

We can specialize each result property with the specific value a Trick, Treat, or NoShow will hold by re-declaring it.

interface Treat extends House { result: "treat", treat: { candy: string, baked?: string } }
interface Trick extends House { result: "trick", trick: string }
interface NoShow extends House { result: "no-show" }

Which would give the exact string value for result in each type.

Our answer.

Intermediate/Advanced Challenge

This one either stumped people for ten minutes or was considered a breeze. We had a few different answers in the TypeScript team, but we think this one is the most elegant.

type Result = {
    done: boolean,
    who: string,
    loot: Record<string, any>
}

type TrunkOrTreatResults = Record<typeof trunkOrTreatSpots[number], Result>

This challenge was a great reminder that a Record is simply a type-alias to a conditional type:

type Record<K extends string | number | symbol, T> = { [P in K]: T; }

Which can convert the original answer:

type Result = {
    done: boolean,
    who: string,
    loot: Record<string, any>
}

type ResultMapper<P extends readonly string[]> = {
    [K in P[number]]: Result
};

type TrunkOrTreatResults = ResultMapper<typeof trunkOrTreatSpots>;

into code which is both easier to read, and requires knowing less language concepts. Kudos to the folks who made it seem very easy!

Here's our answer. Bonus: the weirdest answer.

The Challenge

Beginner/Learner Challenge

You've been keeping tally of how the houses on your street respond to trick-or-treaters. Can you reduce duplication from the types needed to describe the results?

Help out here.

Intermediate/Advanced Challenge

You have a list of trunk or treat spots, in your rush you hardcoded the result of your map function to convert it from an array to an object. Now the list is longer than three items, it's time to map that hardcoded result into a type. Can you refactor this TODO list function?

Refactor.

Need Extra Help?

If you need additional help you can utilize the following:

Happy Typing :)

// No-one knows what is going on, but you think your workplace
// is haunted by a poltergeist. It kinda sucks.
//
// Your objects keep getting changed behind the scenes,
// stop this from happening by making your objects readonly
// to stop this spooky behavior.
type Rectory = {
rooms: Room[]
noises: any[]
}
type Room = {
name: string
doors: number
windows: number
ghost?: any
}
// http://www.unexplainedstuff.com/Ghosts-and-Phantoms/Famous-Haunted-Houses-and-Places-Epworth-rectory.html
const epworthRectory = {
// https://epwortholdrectory.org.uk/epworth-old-rectory-old/visit/virtual-tour/
rooms: [
{ name: "Entrance Hall", doors: 5, windows: 2 },
{ name: "The Kitchen", doors: 2, windows: 3 },
{ name: "Parlour", doors: 2, windows: 3 },
{ name: "The Nursery", doors: 2, windows: 3 },
{ name: "The Attic", doors: 2, windows: 1 },
{ name: "The Study", doors: 2, windows: 2 },
{ name: "Master Bedroom", doors: 1, windows: 4 },
{ name: "Bedroom 2", doors: 2, windows: 3 }
]
}
// This code should cause compiler errors
function haunt(rectory: Rectory) {
const room = rectory.rooms[Math.floor(Math.random() * rectory.rooms.length)];
room.ghost = { name: "Old Jeffrey" }
}
// You got roped into the cutest halloween competition, judging
// doggy halloween pet costumes at the annual parade.
declare function decideWinner(breed: string, costume: string): { name: string, video: string }
window.decideWinner = someoneElseDecides
// Oh, actually you didn't - someone else got to do the fun bit...
// Though you can watch it on zoom: http://www.tompkinssquaredogrun.com/halloween
// Instead, you've been asked to help tally up a scoreboard of the most
// popular costumes according to the most popular breeds. You've built
// out a quick implementation below, but it loses type information.
// Now the contest is over, you feel it's your duty to refactor this
// code to retain type information - you've heard that the 4.1 beta includes
// something which helps with typing string manipulation.
const breeds = ["Hound" , "Corgi" , "Pomeranian"] as const
const costumes = ["Pumpkin" , "Hot Dog" , "Bumble Bee"] as const
function tallyPopularWinners(_breeds: typeof breeds, _costumes: typeof costumes) {
const winners: Record<string, any> = {}
for (const breed of _breeds) {
for (const costume of _costumes) {
const id = `${breed}-${costume}`.toLowerCase()
winners[id] = decideWinner(breed, costume)
}
}
return winners
}
// You can run this example in order to see what the shape of the data looks like, but
// the result will have keys which are lowercased for every mix of breed and costume, e.g:
// {
// "hound-pumpkin": {...},
// "hound-hot dog": {...},
// "hound-bumble bee": {...},
// "corgi-pumpkin": {...}
// ...
// }
const winners = tallyPopularWinners(breeds, costumes)
console.log(winners)
// Passes
winners["hound-pumpkin"].name
// Should fail
winners["pumpkin-pumpkin"].video
// Spoilerific tips below:
// 1: This builds off ideas from day 3, which builds of previous days too
// 2: The implementation will require an `as` or two alas, but with some
// work you can partially find an answer
// Mainly just adding a quick implementation, so that you can run the
// code above without a problem:
function someoneElseDecides(_breed: string, _costume: string): { name: string, video: string }{
// Yes, all these dogs have a name which is just a hexcode... hah
const genRanHex = (size: number) => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
return {
name: genRanHex(6),
video: genRanHex(6)
}
}

Day 4's solutions

Beginner/Learner Challenge

We were looking for using the Readonly utility type to force the type to not allow poltergeists to make changes to your rooms.

- type Rectory = {
+ type Rectory = Readonly<{
    rooms: Room[]
    noises: any[]
- }
+ }>

- type Room = {
+ type Room = Readonly<{
    name: string
    doors: number
    windows: number
    ghost?: any
- }
+ }>

It's worth remembering that JavaScript doesn't have immutability like this, so this really only affects the type system. You'll get a compiler error, but people can still work around that.

Our answer

Intermediate/Advanced Challenge

This challenge evolved quite naturally from the third challenge with the addition of 4.1's heading feature Template Literals.

The most elegant answer moved the return type into the object const winners which would then be inferred as the return type of tallyPopularWinners:

const breeds = ["Hound" , "Corgi" , "Pomeranian"] as const
const costumes = ["Pumpkin" , "Hot Dog" , "Bumble Bee"] as const

+ type Breed = typeof breeds[number]
+ type Costume = typeof costumes[number]
+ type BreedCostumeCombination = `${lowercase typeof breeds[number]}-${lowercase typeof costumes[number]}`

function tallyPopularWinners(_breeds: typeof breeds, _costumes: typeof costumes) {
-  const winners = {} as any
+  const winners: Record<BreedCostumeCombination, ReturnType<typeof decideWinner>> = {} as any

Our answer Also, we watched the full two hours of that video, and it's a great background video while you're working.

The Challenge

Beginner/Learner Challenge

You're in charge of restocking the houses on your street, can you find a way to reduce the duplication in your notes by generically declaring a house?

Help keep those houses DRY.

Intermediate/Advanced Challenge

You're working on a horror movie night. You've skipped the types because you figured it'd be simple to work with but someone put the wrong movie in and The Nightmare Before Christmas is not a halloween movie. It's in the name. Anyway. To avoid this happening again, you figure it's time to add types to the function. Once, you got that down then you wonder if you can do the same thing for the kids schedule?

Help type the schedule.

Need Extra Help?

If you need additional help you can utilize the following:

Happy Typing :)

// We have a set of houses on a street which
// gives out candy and occasionally needs re-stocking.
// This is an accurate representation of the houses
// but there's a lot of duplicated code in here, and
// you want to create a type which could represent
// any house.
// It looks like the only thing which changes between
// houses is the return value of `trickOrTreat` and
// the first parameter of `restock`.
// Can you use a generic type to make a `House` type
// which has a single argument for the return value
// of `trickOrTreat` and the param to `restock`?
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;
};
type FourthHouse = {
doorNumber: 4;
trickOrTreat(): "candy" | "spooky spiders";
restock(items: "candy" | "spooky spiders"): void;
};
type Street = [FirstHouse, SecondHouse, ThirdHouse, FourthHouse];
// 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.
const moviesToShow = {
halloween: { forKids: false },
nightmareOnElmStreet: { forKids: false },
hocusPocus: { forKids: true },
theWorstWitch: { forKids: true },
sleepyHollow: { forKids: false }
} as const
// They got away with this travesty because you have some `any`s in the
// codebase for creating the scheduler. An OK call for a first pass, but
// we're sharing code with others and want to be explicit.
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
}
// Creates a scheduler
const movieNight = makeScheduler(moviesToShow)
// Then all these functions are automatically created
movieNight.getVHSForHalloween()
movieNight.makePopcornForHalloween()
movieNight.playHalloween()
// Not a halloween movie! This should be a compiler error
movieNight.getVHSForNightmareBeforeChristmas()
movieNight.makePopcornForNightmareBeforeChristmas()
movieNight.playNightmareBeforeChristmas()
movieNight.getVHSForHocusFocus()
movieNight.makePopcornForHocusPocus()
movieNight.playHocusPocus()
// Spoilers and part 2 of this challenge are below
// Clue 1. This challenge builds off the last one, which builds off
// the last one, etc...
// Clue 2. These stringly typed function names can probably be handled
// with a new feature from TypeScript 4.1
// **Part two**
// You have the code to organize the same schedule, but with only kids movies.
// It'd be great if you could apply the same types - but only when a movie
// has been declared to be a kids movie.
function makeKidScheduler(movies: typeof moviesToShow): any {
const schedule = {} as any
for (const movie in Object.keys(movies)) {
// @ts-ignore
if (moviesToShow[movie].forKids) {
const capitalName = movie.charAt(0).toUpperCase() + movie.slice(1);
schedule[`getVHSFor${capitalName}`] = () => {}
schedule[`makePopcornFor${capitalName}`] = () => {}
schedule[`play${capitalName}`] = () => {}
}
}
return schedule
}
// Then when we have an object which uses these types
const kidsMovieNight = makeKidScheduler(moviesToShow)
// Should fail
kidsMovieNight.getVHSForHalloween()
kidsMovieNight.playHalloween()
// Should pass
kidsMovieNight.getVHSForHocusFocus()
kidsMovieNight.makePopcornForHocusPocus()
kidsMovieNight.playHocusPocus()
// Spoiler clues below
// 1. You can re-use your template literal types, but you need a new mapped type.
// 2. Finding the right spot for your conditional is tricky, but there aren't too many positions
// where you can use them.
// 3. You may need to apply additional contraints to your mapped type from the first one

Day 5's solutions

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

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

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 }

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>

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;
};

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`

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> {

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;
};

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

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