DEV Community

Cover image for Type | Treat 2021 - Day 4
Orta
Orta

Posted on

Type | Treat 2021 - Day 4

Type | Treat Challenge 4

Welcome to the fourth Type | Treat challenge! These challenges are a series of blog posts which have 2 code challenges in, one for beginners and one for intermediate TypeScript programmers. We're on day four, which means going over the answers from yesterday and have 2 new challenges.

Yesterday's Solution

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

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

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

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

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

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

Click to learn about these cases

req(`${0.3e21}cm`)
Enter fullscreen mode Exit fullscreen mode

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

req(`${Infinity}cm`)
req(`${NaN}cm`)
Enter fullscreen mode Exit fullscreen mode

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

- type Length = `${number}${Space}${Unit}`
+ type Length = `${bigint}${Space}${Unit}`
Enter fullscreen mode Exit fullscreen mode

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

Which describes both possible 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';
Enter fullscreen mode Exit fullscreen mode

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

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

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

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>". That change 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]
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

Our answer.

The Challenge

Beginner/Learner Challenge

Make some candy bowls. Then make some very specific bowls.

Intermediate/Advanced Challenge

Run a set of pumpkin competitions

How To Share Your Solution

Once you feel you have completed the challenge, you will need to select the Share button in the playground. This will automatically copy a playground URL 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 here!

Best Resources for Additional Help

If you need additional help you can utilize the following:

Happy Typing :)

Top comments (4)

Collapse
 
brunnerh profile image
brunnerh • Edited

There seems to be an error in the intermediate problem's check implementation. The judge function arguments should probably be unknown[] rather than [unknown], otherwise it will still cause an error after implementing the solution.

Solution

Collapse
 
orta profile image
Orta

Thanks - fixed!

Collapse
 
leko profile image
Leko

My solution: Intermediate

Collapse
 
drzamich profile image
Michał Drzazga

Here's my go at the intermediate challenge