DEV Community

Cover image for Type | Treat 2021 - Wrap-up
Orta for typescript

Posted on

Type | Treat 2021 - Wrap-up

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

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

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

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

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

Which would cause a compiler error with previousScheme - letting you know it worked.

Our answer

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

To:

function handleSale(events: { [Book in Books as `on${Book["genre"]}`]?: (e: Book) => void }) {
  // ... 
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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.

Our answer.

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.

Top comments (1)

Collapse
 
brunnerh profile image
brunnerh

Did not know that in could be used on unions; that is very convenient!

My solution was a bit more verbose due to requiring a type narrowing on the callback argument, but it works as well:

function handleSale(events: {
  [genre in Books['genre'] as `on${genre}`]?:
    (e: Books & { genre: genre }) => void
}) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode