Percentage Parser
Percentage Parser
Challenge
Implement PercentageParser<T extends string>.
According to the regex /^(\+|\-)?(\d*)?(\%)?$/, match T and extract three capture groups.
The structure should be: [sign, number, unit]
If a group is not captured, default it to an empty string ''.
type PString1 = ""
type PString2 = "+85%"
type PString3 = "-85%"
type PString4 = "85%"
type PString5 = "85"
type R1 = PercentageParser<PString1> // ['', '', '']
type R2 = PercentageParser<PString2> // ['+', '85', '%']
type R3 = PercentageParser<PString3> // ['-', '85', '%']
type R4 = PercentageParser<PString4> // ['', '85', '%']
type R5 = PercentageParser<PString5> // ['', '85', '']Solution
type ParseSign<T extends string> =
T extends `${'+' | '-'}${string}` ? T extends `${infer S}${string}` ? S : '' : ''
type ParseUnit<T extends string> =
T extends `${string}%` ? '%' : ''
type ParseNumber<T extends string, S extends string, U extends string> =
T extends `${S}${infer N}${U}` ? N : ''
type PercentageParser<T extends string> =
T extends `${infer A}${infer _Rest}`
? [
ParseSign<T>,
ParseNumber<T, ParseSign<T>, ParseUnit<T>>,
ParseUnit<T>
]
: ['', '', '']Actually, the cleanest solution leverages a single conditional type chain:
type PercentageParser<T extends string> =
T extends `${'+' | '-'}${infer Rest}`
? T extends `${infer Sign}${Rest}`
? Rest extends `${infer Num}%`
? [Sign, Num, '%']
: [Sign, Rest, '']
: ['', '', '']
: T extends `${infer Num}%`
? ['', Num, '%']
: ['', T, '']Breaking it down
Step 1 — Detect optional sign
T extends `${'+' | '-'}${infer Rest}`If T starts with + or -, we match the sign and capture the Rest of the string.
The union '+' | '-' acts as an OR — TypeScript distributes over it.
Step 2 — Extract the sign character
T extends `${infer Sign}${Rest}`We already know T starts with a sign, so this extracts Sign as a single-char prefix. Since Rest is known, TypeScript pins Sign to exactly the first character.
Step 3 — Detect optional % suffix in the rest
Rest extends `${infer Num}%`If the remaining string ends with %, extract the number part Num and use '%' as unit. Otherwise the unit is ''.
Step 4 — Handle no-sign case
T extends `${infer Num}%`
? ['', Num, '%']
: ['', T, '']When there's no sign, check if T ends with %. If yes, return ['', Num, '%']; otherwise the whole string is the number with no unit.
Example trace
PercentageParser<'+85%'>
→ matches '+' | '-' prefix → Rest = '85%'
→ Sign = '+'
→ '85%' extends `${infer Num}%` → Num = '85'
→ ['+', '85', '%'] ✓
PercentageParser<'85%'>
→ does NOT match '+' | '-' prefix
→ '85%' extends `${infer Num}%` → Num = '85'
→ ['', '85', '%'] ✓
PercentageParser<''>
→ does NOT match '+' | '-' prefix
→ '' does NOT match `${infer Num}%`
→ ['', '', ''] ✓Deep Dive
Why T extends \${'+' | '-'}${infer Rest}`` works as an OR
TypeScript distributes union types in conditional type position. Writing '+' | '-' inside a template literal is syntactic sugar for:
"Does
Tmatch the pattern starting with+, OR the pattern starting with-?"
Both branches resolve to the same Rest type (the tail of the string), so the union collapses cleanly.
The two-step sign extraction trick
You might wonder: if we matched T extends \${'+' | '-'}${infer Rest}``, why not directly have the sign available?
The catch is that the literal '+' | '-' is a union, not a captured infer. We can't name it directly. The workaround is to then ask:
T extends `${infer Sign}${Rest}`Here Rest is now a known concrete type (inferred in the outer extends), so TypeScript can pin Sign to exactly the one remaining prefix character. This is a common pattern in type-level string parsing.
Comparison with a helper-type approach
Some community solutions split the logic into three small helper types:
type GetSign<T extends string> =
T extends `${'+' | '-'}${string}` ? (T extends `${infer S}${string}` ? S : never) : ''
type GetUnit<T extends string> =
T extends `${string}%` ? '%' : ''
type GetNumber<T extends string,
S extends string = GetSign<T>,
U extends string = GetUnit<T>> =
T extends `${S}${infer N}${U}` ? N : ''
type PercentageParser<T extends string> =
[GetSign<T>, GetNumber<T>, GetUnit<T>]This is arguably more readable (each helper has a single responsibility) and closely mirrors how you'd decompose the regex /^(\+|\-)?(\d*)?(\%)?$/ into three independent capture groups.
The trade-off: the nested single-chain version avoids re-evaluating T three times, which can matter for type-checking performance at scale.
Edge cases to watch
| Input | Expected | Notes |
|---|---|---|
"" | ['', '', ''] | Empty string — all groups default to '' |
"+" | ['+', '', ''] | Sign only, no digits, no unit |
"%" | ['', '', '%'] | Unit only |
"+%" | ['+', '', '%'] | Sign + unit, no digits |
"-0%" | ['-', '0', '%'] | Negative zero percent |
Relation to other string-parsing challenges
| Challenge | Technique |
|---|---|
Trim (108) | Remove prefix/suffix characters |
Replace (116) | Split on a substring |
StartsWith (2688) | Check prefix with template literal |
EndsWith (2693) | Check suffix with template literal |
PercentageParser (1978) | Extract prefix + middle + suffix with conditional chain |
Split (2822-hard) | Recursive split on delimiter |
PercentageParser is a clean exercise in combining prefix detection (StartsWith-style) with suffix detection (EndsWith-style) in a single type expression.
Key Takeaways
- Union in template literals (
'+' | '-') acts as a type-level OR for prefix matching - Two-step sign extraction: first detect the union prefix, then re-match with
inferto capture the concrete character - Suffix detection (
T extends \${infer N}%`) mirrorsEndsWith` — natural for optional trailing characters - The helper-type decomposition (one type per capture group) closely mirrors regex groups and is highly readable
- This pattern generalises to any
[prefix, body, suffix]parsing problem at the type level
