Drop Char
Drop Char
Challenge
Drop a specified character from a string.
type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'Solution
type DropChar<S extends string, C extends string> =
S extends `${infer L}${C}${infer R}`
? DropChar<`${L}${R}`, C>
: SThe idea: use template literal inference to find and remove one occurrence of C at a time, then recurse until no more occurrences remain.
Breaking it down
Step 1 — Pattern match with template literal infer
S extends `${infer L}${C}${infer R}`TypeScript will try to split S into three parts:
L— everything before the first occurrence ofCC— the character to drop (matched literally)R— everything after the first occurrence
If C appears anywhere in S, this branch resolves to true and we get L and R.
Step 2 — Recurse with C removed
? DropChar<`${L}${R}`, C>We reassemble the string without C, then recurse to catch the next occurrence.
Step 3 — Base case
: SWhen S no longer contains C, the extends check fails and we return S as-is.
Example trace
DropChar<' b u t ', ' '>
→ '' + 'b u t ' → DropChar<'b u t ', ' '>
→ 'b' + 'u t ' → DropChar<'bu t ', ' '>
→ 'bu' + 't ' → DropChar<'but ', ' '>
→ 'but' + '' → DropChar<'but', ' '>
→ 'but' (no ' ' found, return as-is)Deep Dive
Why does single-character C work cleanly?
The challenge constrains C to a single character (a string literal of length 1). This matters because when C is a single char, the template literal ${infer L}${C}${infer R} has a unique split at each occurrence — TypeScript will always find the leftmost occurrence first, making the recursion deterministic.
If C were a multi-character string (e.g., "ab"), the same pattern would still work for full substrings, which is exactly what the harder sibling challenge Drop String (2059) explores (where C is a union of chars rather than a single string).
Template literal types as pattern matching
TypeScript's template literal inference (${infer X}) is essentially structural pattern matching on string types. A few things to know:
- Greedy from the left by default —
${infer L}${C}${infer R}makesLas short as possible (leftmost match). - Exact literal match —
${C}matches the exact string value ofC. IfC = ' ', TypeScript looks for a literal space. - Works recursively — since conditional types can recurse, this enables full string traversal.
Alternative: Character-set approach
If we only need to drop single chars, we could also approach it with a union of individual chars:
// This doesn't directly work, but illustrates the idea:
type DropChar<S extends string, C extends string> =
S extends `${infer Head}${infer Tail}`
? Head extends C
? DropChar<Tail, C>
: `${Head}${DropChar<Tail, C>}`
: SThis version processes character-by-character: infer one Head char at a time, skip it if it equals C, otherwise keep it. This is more verbose but makes the character-by-character logic explicit.
The original two-infer version is shorter and preferred.
Relationship to other string manipulation challenges
| Challenge | Technique |
|---|---|
TrimLeft (106) | Remove leading whitespace with ${' ' | '\n' | '\t'}${infer R} |
Trim (108) | Trim both ends |
Replace (116) | Replace first occurrence |
ReplaceAll (119) | Replace all occurrences (same recursion pattern as DropChar) |
DropChar (2070) | Remove all occurrences of a char |
DropString (2059-hard) | Remove all chars in a character-set union |
DropChar is essentially ReplaceAll<S, C, ''> — replacing every C with an empty string.
TypeScript recursion limits
TypeScript has a recursion depth limit (roughly 1000 conditional type instantiations). For very long strings this solution could hit that limit. In practice, for typical string sizes (< a few hundred characters) this is never a concern.
Key Takeaways
- Template literal + dual
inferis the idiomatic way to search and manipulate substrings at the type level - Recursion drives the "remove all occurrences" behaviour — one removal per recursive call
- The base case (
Scontains noC) is implicit in theextendsfailing and returningS DropChar<S, C>is equivalent toReplaceAll<S, C, ''>— understanding the two together reinforces the pattern- This pattern scales to harder challenges like
DropString(remove a set of chars) by changingCto a union
