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
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 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';
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.
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:
-
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>` }
-
Override
toUpperCase
to support template literals:
interface String { toUpperCase<T extends string>(this: T) : Uppercase<T> }
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]
}
}
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.
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:
- The New TypeScript Handbook
- The TypeScript Community Discord
- The comments on each Dev.to post!
- Our previous
Type | Treat
2020 challenges
Happy Typing :)
Top comments (4)
There seems to be an error in the intermediate problem's
check
implementation. Thejudge
function arguments should probably beunknown[]
rather than[unknown]
, otherwise it will still cause an error after implementing the solution.Solution
Thanks - fixed!
My solution: Intermediate
Here's my go at the intermediate challenge