Advanced Template Literals Techniques in TypeScript
We recently talked about template literals and how you can use them to do different types of checks as well as string manipulation. Today we are going to take it one step further and expand our knowledge of template literals and combine them with other TypeScript fundamentals to do some really cool things.
The last post laid the foundation, setting the stage for today's challenge. If you haven't taken a look, it's a valuable read that will come in handy for what's up next!
Let's jump into the challenge.
Day fourteen - Dec 14
I strongly encourage you to try the first challenge on your own before reading the solution here.
Challenge: Naughty List Decipher
The challenge
- [early on the morning of Thursday December 14th, Santa stumbles into office greeted by Bernard, the head elf..]*
[Bernard] YOU'RE A MESS. Were you out partying.. on a WEDNESDAY?? AGAIN??!!!
[Santa] It seems as such. Some investors were in town so we went over to the Mistletoe Lounge and things got a little out of hand.
[Bernard] I oughta report you to HR. Seriously. This is getting out of control.
[Santa] We're like a family here; no need for formal HR processes!
[Bernard] Where's the list for today's naughty kids? We're behind on coal lump production.
[Santa] Umm.
[Bernard] You're joking. Tell me you're joking. You lost the list again?
[Santa] Well, not lost per se.
[Bernard] Then where is it?
[Santa] I have it.. but I only scribbled down the names real quick with slashes in between them.
Covering for Santa, again.
Looks like we're gonna need to pick up the slack for Santa yet again. He's got a list like "melkey/prime/theo/trash"
and we need to turn it into a union of strings "melkey" | "prime" | "theo" | "trash"
.
Let's get this done before the rest of the elves find out.
The code to complete
type DecipherNaughtyList = unknown;
The tests
import { Expect, Equal } from 'type-testing';
type test_0_actual = DecipherNaughtyList<'timmy/jimmy'>;
type test_0_expected = 'jimmy' | 'timmy';
type test_0 = Expect<Equal<test_0_expected, test_0_actual>>;
type test_1_actual = DecipherNaughtyList<'elliot'>;
type test_1_expected = 'elliot';
type test_1 = Expect<Equal<test_1_expected, test_1_actual>>;
type test_2_actual = DecipherNaughtyList<'melkey/prime/theo/trash'>;
type test_2_expected = 'melkey' | 'prime' | 'theo' | 'trash';
type test_2 = Expect<Equal<test_2_expected, test_2_actual>>;
The solution
type DecipherNaughtyList<T extends string> =
T extends `${infer Head}/${infer Tail}`
? Head | DecipherNaughtyList<Tail>
: T;
Right off the bat, you may notice concepts that we have covered multiple times in this series and a couple of new ones as well. Things like generic and conditional types, and the infer and extends keywords are topics that have helped us in the past to solve some challenges.
Let's start breaking down the solution:
Type Definition:
type DecipherNaughtyList<T extends string>
We are starting by defining a generic type parameter T
that extends string
, meaning that our input should always be of type string
.
Conditional Type:
T extends `${infer Head}/${infer Tail}`
? Head | DecipherNaughtyList<Tail>
: T;
DecipherNaughtyList
is equal to the result of the above conditional. Which checks if the input string T
can be split into two parts separated by a '/'
.extends
is making sure that T
is a string in the form of "something/somethingElse"
.
If T
can be split, it takes the first part (Head
) and recursively calls DecipherNaughtyList
In the second part (Tail
). If not, it simply returns the original string T
.
And yes, my friends... we are using our beloved recursion to solve another challenge. Fun times, right?
Recursive Splitting:
`${infer Head}/${infer Tail}`
The above statement is a template literal that represents the part where the recursive splitting happens. This will split the string at the first occurrence of '/'
.
Head
represents the part before '/'
, and Tail
represents the part after it. But we can use any other names like Prefix
and Rest
.
The infer
keyword is needed to allow TypeScript to infer the types of Head
and Tail
based on the structure of the string, in other words, making sure we are always using a string
in the template literal.
This way of using template literals took me a bit to "fully" learn and grasp, so just in case, I want to reiterate how this is working.${infer Head}/${infer Tail}
is like saying, "Hey TypeScript, if my string looks like 'something/somethingElse'
, figure out and remember what 'something' is and call it Head
, and what 'somethingElse' is and call it Tail
for me."
At the end, the type combines Head
with the result of the recursive call on Tail
, resulting in a union type.
Head | DecipherNaughtyList<Tail>
Base Case:
T extends `${infer Head}/${infer Tail}` ? ... : T;
Finally, if the string cannot be split further meaning that there are no more '/'
, it returns the original string T
. This is the base case for the recursion.
Breaking it down with an example
Now, let's walk through how our solution works using the following example:
DecipherNaughtyList<'melkey/prime/theo/trash'>;
Initial Call: The input is
'melkey/prime/theo/trash'
. The type checks if it can be split into${infer Head}/${infer Tail}
.First Split: It splits into
Head = 'melkey'
andTail = 'prime/theo/trash'
. The type then becomes'melkey' | DecipherNaughtyList<'prime/theo/trash'>
.Recursive Call: Now, the type calls itself with the input
'prime/theo/trash'
. This follows the same logic:It splits into
Head = 'prime'
andTail = 'theo/trash'
.The type becomes
'prime' | DecipherNaughtyList<'theo/trash'>
.
Second Recursive Call: Continuing the recursion, it now calls itself with the input
'theo/trash'
:It splits into
Head = 'theo'
andTail = 'trash'
.The type becomes
'theo' | DecipherNaughtyList<'trash'>
.
Base Case: Now, the recursion stops because the input
'trash'
cannot be split further. The type returns the original string'trash'
.Building the Union: While the recursion is happening, the type combines the results of each recursive call. The union is constructed by taking the
Head
at each step:'melkey' | ('prime' | ('theo' | 'trash'))
Final Union: The union simplifies to
'melkey' | 'prime' | 'theo' | 'trash'
.
Today we really covered a lot of ground, I hope you should be feeling like a template literal master now.
As we wrap up another day of TypeScript exploration, we're also wrapping up presents โ getting closer to Christmas folks!
If you like this content consider checking out what I post on Twitter/X ๐ฆ
Subscribe to my newsletter
Read articles from Fernando De La Madrid directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Fernando De La Madrid
Fernando De La Madrid
Bury the past, code the future!