Type | Treat Challenge Wrap-up
That's all the challenges done! If you missed any of the challenges they're all available in the Playground here. Let's talk through the final answers to day 5's challenges.
Yesterday's Solution
Beginner/Learner Challenge
In this challenge we were working with an existing object literal which had been as const
d. The goal was to use keyof
and typeof
to extract interesting sets of strings from the object. The first being the keys in the object:
- type SchemeNames = "background" | "textColor" | "highlightOne"
+ type SchemaNames = keyof typeof scheme
Idealy you might have noticed the 'key of' in the comment, which could have led you to keyof
, from there you needed to re-use typeof
to extract the type from scheme
. This gets all of the keys from that constant.
Next, we asked if you could improve
function possibleSchemeItems(colors: any): string[] {
const keys = Object.keys(colors) as string[]
return keys
}
In this case, you can switch string
to SchemeNames
- this example was made so that there was an interesting function to play around with. You could switch colors
to typeof schema
but nothing you do around Object.keys
would keep the types retained. This is kind of interesting, the reasoning is that TypeScript cannot make reasonable guarantees that the return value of Object.keys
actually is keyof typeof colors
(because of JavaScript prototype chaining) and so you have to re-type that line. Interesting.
function possibleSchemeItems(colors: typeof scheme): SchemaNames[] {
const keys = Object.keys(colors) as SchemaNames[]
return keys
}
Next we had you figure out how to get the values of schema
as a union:
- type PossibleColors = '#242424' | '#ffa52d' | '#cafe37'
+ type PossibleColors = typeof scheme[SchemaNames]
We tried to be quite explicit in the clue leading up to this one, because jumping to an indexed type might not be instinctive for beginners. This we considered to be the trickiest part of the challenge.
Then to wrap up the challenge, and to make sure the types you are creating are constructive in a real world-ish scenario, we presented a Record
to see if you knew that the first parameter can be a list of keys:
- type Scheme = Record<string, string>;
+ type Scheme = Record<SchemaNames, string>;
Which would cause a compiler error with previousScheme
- letting you know it worked.
Intermediate/Advanced Challenge
For this challenge, we started with the type we wanted you to write, and then tried to find incremental steps which built up in complexity till you hit it.
The total change we were looking for was this:
function handleSale(events: Record<string, (e: Books) => void>) {
// ...
}
To:
function handleSale(events: { [Book in Books as `on${Book["genre"]}`]?: (e: Book) => void }) {
// ...
}
Getting there is a bit of a jump!
We first asked you to switch the string
in the Record
to be Books
:
- function handleSale(events: Record<string, (e: Books) => void>) {
+ function handleSale(events: Record<keyof IncomingBookMap, (e: Books) => void>) {
The next step was to remove the Record
and replace it with its underlaying mapped type:
- function handleSale(events: Record<string, (e: Books) => void>) {
+ function handleSale(events: { [Book in keyof IncomingBookMap]?: (e: Books) => void }) {
The ?
being the key that if you searched the TypeScript documentation for "mapping modifier" you'd see the documentation around mapped types which we also improved in the process of making this challenge.
We thought that could be enough for some folk, and that was a good pausing point and thus classed the final change as a bonus. That jump revolved around using key remapping via as
in the mapped type.
- function handleSale(events: { [Book in keyof IncomingBookMap]?: (e: Books) => void }) {
+ function handleSale(events: { [Book in Books as `on${Book["genre"]}`]?: (e: Book) => void }) {
The IncomingBookMap
existed specifically to add the prefix on
to the genre of each book, which is handled by template string literals. All of the documentation for these concepts are in the same page, so after a few reads it should hopefully all make sense.
This system is more or less how the DOM's event system works, and is what the idea is loosely based on, from lib.dom.d.ts
:
interface DocumentAndElementEventHandlersEventMap {
"copy": ClipboardEvent;
"cut": ClipboardEvent;
"paste": ClipboardEvent;
}
interface DocumentAndElementEventHandlers {
addEventListener<K extends keyof DocumentAndElementEventHandlersEventMap>(type: K, listener: (this: DocumentAndElementEventHandlers, ev: DocumentAndElementEventHandlersEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
}
Roughly translates to english as "DocumentAndElementEventHandlersEventMap" shows how to handle document.addEventListener("copy", (evt) => { ... })
and what to map the first argument of that function to. The DOM types can't really switch to use a template literal because not all properties have an on
prefix, but it's a pretty logical expansion of the idea in specific cases.
Thanks!
There are a few people involved in getting Types | Treat
running whose name you don't see on the masthead: Gabriella and Daniel on the TypeScript team, and the folks in the TypeScript Community Discord - especially webstrand and michael. My wife, Danger, should probably also get a shout for helping to provide themes - we've done 40 challenges so far and have to dig pretty deep occasionally to be fun and topical.
Feedback
If you gave Types | Treat
a shot, we love for you to run through our 6 question survey which help us figure out how to change and improve. Feedback on Twitter is useful, but there's only so much context you can gve in 280 characters!
From here?
If you've not done the 2020 Type | Treat
, it's also good!
If you want to challenge yourself further, check out types-challenges this clever system of type challenges goes all the way up to "very challenging for the TypeScript team" and also run entirely in the TypeScript Playgound. It's great work.