The WTF Moment
Have you ever come across something like this in Typescript and thought "Where do I start if I want to understand this?"
export type Split<S, TIncludeTrailingSlash = true> = S extends unknown
? string extends S
? string[]
: S extends string
? CleanPath<S> extends ''
? []
: TIncludeTrailingSlash extends true
? CleanPath<S> extends `${infer T}/`
? [...Split<T>, '/']
: CleanPath<S> extends `/${infer U}`
? Split<U>
: CleanPath<S> extends `${infer T}/${infer U}`
? [...Split<T>, ...Split<U>]
: [S]
: CleanPath<S> extends `${infer T}/${infer U}`
? [...Split<T>, ...Split<U>]
: S extends string
? [S]
: never
: never
: never
(This particular example comes from Tanstack Router.)
This doesn’t look like type annotations on top of JS—it looks like a whole different language! Types like this do not show up often in our application code, but they can show up in open source libraries that want to handle a variety of inputs and outputs to help out in lots of use cases. Sometimes I act as a library developer building tools for Rangle’s clients, and I want to understand, modify, or even write code like this so the product developers using my libraries can write type-safe code.
I realized other devs at Rangle felt as lost as I did when looking at code like Split, so I decided to help by leading a group of Ranglers through the "easy" set of 13 challenges from the Type Challenges repository over a series of Zoom calls and Slack conversations. At the start, I could not solve any of them, but after a few hours of struggling, giving and receiving help, and Googling, I could get through most of them and help others as well. I could also almost understand how that Split type above works. Now I'd like to introduce you to the same set of challenges and provide some advice on solving and learning from them.
Introducing Type Challenges
The Type Challenges are a series of programming puzzles that exist within TS’ type system. The puzzles present the programmer with an incomplete type (here’s the “warm–up” puzzle as an example), and the programmer’s goal is to finish the type, eliminating all the TS errors.
Pressing the blue “Take the Challenge” button on each puzzle’s page leads the programmer to the TS Playground where they can use a browser-based IDE to work on the puzzle with instant feedback from the TS language server. This makes experimenting with different solutions extremely quick.
When the programmer wants a hint or to compare their solution with others, they can view a list community-submitted of solutions in the repo’s GitHub Issues. When the programmer has solved the puzzle by eliminating type errors, they can optionally submit their own solution for others to learn from later.
Why try these programming puzzles?
Solving these puzzles isn’t very practical. Many of them are duplicates of utility types built into Typescript, and if you don’t read/write library-ish code you may never need to use these skills directly. I think there are still some benefits though:
- You are expanding the depth of your Typescript knowledge. More of the code you come across will be less intimidating because you'll be able to read, understand, or modify it. You’ll be able to help your teammates when they face similar issues too.
- You are learning new debugging skills. As you become more comfortable with the TS playground you may find uses for it in your day-to-day work like testing code in different versions of TS, testing the impact of changing different TS settings, or using the // ^? helper.
- You are growing more confident in the TS compiler to catch errors, and will start introducing stricter types into the code you write. Maybe you’ll find new places to use syntax like as const, or introduce libraries like Zod to your project. You might become comfortable updating your TS config files to use stricter checks, helping you write fewer unit tests and more reliable software.
General Suggestions for Type Programming Puzzles
1. I’ve grouped the easy puzzles by the concepts you will learn to solve each one. I think solving them in this order is less overwhelming than the order they are listed in the GitHub repo.
2. Look at the solutions within a minute or two of starting a challenge if you are struggling. This is more like learning a new language than solving a LeetCode programming problem—you might not know what TS offers to help you solve a challenge. You’ll find multiple solutions for each problem on the problem pages linked below (look for the pink “View the solution” button), and I’ve added links to explanations on YouTube and other websites as well.
3. If generic type variables with names like T and K are confusing in code like this:
type MyPick<T, K extends keyof T>
do not hesitate to give them more descriptive names like this:
type MyPick<ObjectType, Keys extends keyof ObjectType>.
They are just parameters, and you can rename them for your own benefit without impacting the outcome of the challenge.
4. A nifty feature of the TS Playground that you’ll use as your editor is the // ^? comment. You can type this out to get the editor to display the same information that would appear if you hovered over a type:
Challenges with ..., extends, and generics
Getting started is the hardest part, so go easy on yourself. These challenges introduce you to generics and using extends to constrain generic types while leveraging the familiar spread operator (...) in a new context.
- Unshift (video solution, written solution)
- Push (video solution, written solution)
- Concat (video solution, written solution)
- Length of Tuple (video solution 1, video solution 2, written solution)
Looking back at the Split type, lines like [...Split<T>, '/'] will be clearer after this, and you'll have exposure to generics which will be useful for many other problems and code in your day-to-day work. For example, you’ll have a better idea of what Promise<string> means.
Challenges with the Ternary Operator
These challenges will introduce you to making decisions using the ternary operator with extends which is a bit like JS’ == but within the type system
- If (video solution, written solution)
- Excludes (video solution, written solution)
- First of array (video solution 1, video solution 2, written solution)
The Split example above has 10 ternary operators. Understanding what the ternary operator does for your types will make deciphering it (and similar code) much easier.
Challenges with Mapped Types
These challenges introduce the idea of changing one type into another using Mapped types.
- Tuple to object (video solution 1, video solution 2, written solution)
- Read-only (video solution 1, video solution 2, written solution)
- Pick (video solution 1, video solution 2, written solution)
The Split example above doesn't use these techniques, but they're still part of the challenges, so try them out while you are here. They will be helpful for the next set.
Challenges with infer
The last few challenges combine techniques you've seen already and add on the infer keyword.
- Parameters (video solution, written solution)
- Awaited (video solution, written solution)
- Includes (video solution, written solution)
The infer keyword is one of the last remaining mysteries of the Split type above (for example, look for CleanPath<S> extends ${infer T}/${infer U}`). More and more lines of it will make sense now.
Happy Solving!
Putting what you’ll learn from the puzzles together and understanding the Split type at the start of the post is still going to be a challenge, but it will be much more approachable when you break it into smaller understandable chunks.