Yuzhe's Blog

yuzhes

Drop Char

Drop Char

Challenge Link

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>
    : S

The 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:

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

: S

When 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:

  1. Greedy from the left by default${infer L}${C}${infer R} makes L as short as possible (leftmost match).
  2. Exact literal match${C} matches the exact string value of C. If C = ' ', TypeScript looks for a literal space.
  3. 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>}`
    : S

This 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

ChallengeTechnique
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