Remove every character of R from the string type S. One recursion turns R into a union of characters, another walks S and filters against it.
A type-level filter over characters: one recursion builds the drop set, another walks the string against it.
DropString<S, R> removes every character that appears in R from the string S. Two techniques carry the solution, and both show up all over advanced TypeScript: converting a string type into a union of its characters (a type-level set), and walking a string character by character while rebuilding it.
For example:
[object Object]Drop the specified chars from a string.
For example:
[object Object]View on GitHub: https://tsch.js.org/2059
Change the following code to make the test cases pass (no type check errors).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
Start with the finished types:
type StringToUnion<S extends string> =
S extends `${infer Ch}${infer Rest}` ? Ch | StringToUnion<Rest> : never
type DropString<S extends string, R extends string> =
S extends `${infer Ch}${infer Rest}`
? Ch extends StringToUnion<R>
? DropString<Rest, R>
: `${Ch}${DropString<Rest, R>}`
: ''Two small recursive types cooperate: one turns the characters to drop into a set, the other filters the input against that set.
StringToUnion, a string as a set of charactersTo ask whether a character should be dropped you need R in a form you can test membership against. In the type system, that form is a union. StringToUnion peels characters off one at a time:
[object Object]The pattern `${infer Ch}${infer Rest}` uses a key inference rule: when two infer placeholders are adjacent, the first matches exactly one character and the second takes the remainder. So 'but' splits into Ch = 'b', Rest = 'ut', and the result is 'b' | StringToUnion<'ut'>, unfolding to 'b' | 'u' | 't' | never. The trailing never comes from the base case (the empty string doesn't match the pattern), and never vanishes from any union it joins.
Two properties of unions make this the right representation:
X extends Union. No loops needed.StringToUnion<'tub'> and StringToUnion<'but'> are the same type, which is why the tests expect identical results for 'but' and 'tub'. Order and repetition in R are irrelevant.DropString uses the same one-character split on S and makes a decision per character:
Ch extends StringToUnion<R>: the character is in the drop set, so it contributes nothing. Recurse with DropString<Rest, R>.`${Ch}${DropString<Rest, R>}` glues the character onto the front of whatever the rest of the string produces.Tracing DropString<'foobar!', 'fb'>:
// 'f' β in 'f' | 'b' β dropped
// 'o' β not in set β 'o' + ...
// 'o' β not in set β 'o' + ...
// 'b' β in set β dropped
// 'a' β 'a' + ... 'r' β 'r' + ... '!' β '!' + ...
// '' β base case β ''
// result: 'ooar!'The base case returns '', the identity element for string concatenation. Every kept character stacks on top of it.
R edge caseThe first test demands DropString<'butter fly!', ''> stay 'butter fly!'. It works without any special handling: StringToUnion<''> is never, and no concrete character satisfies Ch extends never, so every character is kept. This is a nice example of choosing a base case (never, the empty union) that makes the general logic degrade gracefully instead of needing a guard.
DropString<' b u t t e r f l y ! ', ' '> strips every space, producing 'butterfly!'.'but' from ' b u t t e r f l y ! ' removes all occurrences of each character. Both ts go, not just the first, because the walk visits every character independently.'but' and 'tub' produce identical results, as noted above.The broader lesson: when a type-level problem says "any of these characters", reach for a union as your set type and pair it with the adjacent-infer single-character walk. The same combination powers most other string-transformation challenges, from Trim to CamelCase.
This challenge is originally from here.