#2059β€’Hard

Drop String

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]

Challenge Instructions: Drop String

Hard

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.

Loading...

Detailed Explanation

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.

Sub-problem 1: StringToUnion, a string as a set of characters

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

Sub-problem 2: the filtering walk

DropString uses the same one-character split on S and makes a decision per character:

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.

The empty R edge case

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

Other edge cases in the tests

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.

Share this challenge

Learn the Concepts