A type-level String.split() that turns a string type into a tuple of substrings. Reproducing JavaScript's odd empty-string rules is the hard part.
The well known split() method splits a string into an array of substrings by looking for a separator, and returns the new array. The goal of this challenge is to do the same in the type system. The happy path isn't the problem. What makes this one hard is faithfully reproducing JavaScript's quirky edge cases: splitting on '' yields individual characters, splitting '' itself behaves differently depending on the separator, and a missing separator returns the whole string untouched.
For example:
[object Object]The well known split() method splits a string into an array of substrings by looking for a separator, and returns the new array. The goal of this challenge is to split a string, by using a separator, but in the type system!
For example:
[object Object]View on GitHub: https://tsch.js.org/2822
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.
The finished type:
type Split<S extends string, SEP extends string = string> =
string extends S
? string[]
: string extends SEP
? [S]
: S extends `${infer Left}${SEP}${infer Rest}`
? [Left, ...Split<Rest, SEP>]
: SEP extends ''
? []
: [S]The core is a three-line recursion; everything else exists to nail the edge cases.
S extends `${infer Left}${SEP}${infer Rest}` searches S for the separator. Template literal inference is lazy, so Left captures the shortest prefix before the first occurrence of SEP, and Rest gets everything after it. We then emit Left and splice in the recursive result with a spread: [Left, ...Split<Rest, SEP>].
For Split<'The sine in cosine', 'in'> the first step evaluates to:
// Left = 'The s'
// Rest = 'e in cosine'
// result so far: ['The s', ...Split<'e in cosine', 'in'>]Repeating this yields ['The s', 'e ', ' cos', 'e']. A multi-character separator that appears mid-word needs no extra code, because the pattern matches substrings, not just single characters.
When S no longer contains the separator, the pattern fails and we fall to the final [S], which wraps the last remaining chunk. That's how 'you?' ends the space-separated example, and how Split<'Hi! How are you?', 'z'> returns the whole string as ['Hi! How are you?'] in one step.
Split<'abc', ''> should return ['a', 'b', 'c'], just like in JavaScript. Interpolating SEP = '' into the pattern leaves `${infer Left}${infer Rest}`, two adjacent infer placeholders. TypeScript resolves this by giving the first placeholder exactly one character, so Left peels off a single character per step and the recursion walks the whole string character by character.
There's a catch at the end, though: when the recursion reaches '', the pattern `${infer Left}${infer Rest}` does not match the empty string (the first placeholder needs at least one character), so we'd fall through to [S] and produce a spurious trailing ''. That's why the fallback checks SEP extends '' ? [] : [S]:
'' and nothing left to split → [], which both terminates the character walk cleanly and makes Split<'', ''> return [] directly, matching ''.split('') in JavaScript.[S], so Split<'', 'z'> correctly yields [''], matching ''.split('z').The tests cover both of these deliberately. Same input string, opposite results, decided purely by the separator.
string extends SEPThe tests also call Split<'Hi! How are you?'> with no separator at all, expecting ['Hi! How are you?'], mirroring str.split(undefined) in JavaScript. We default the parameter to the wide type: SEP extends string = string.
Now, how do you detect "the caller gave me string, not a literal"? The idiom is string extends SEP. For any literal like ' ' or 'z', string extends ' ' is false: the wide type is not assignable to a narrower literal. Only when SEP is string itself does the check pass, and then we return [S] unsplit. This "is it a wide string?" test is worth memorizing; it comes up constantly in type-level string code.
string extends SThe very first branch, string extends S ? string[], does the same trick for the input. If someone writes Split<string, 'whatever'>, there's no concrete string to inspect, so the honest answer is string[]: an array of unknown length with unknown contents. Without this guard the pattern match would misbehave on the wide type. Putting it first means every later branch can safely assume S is a concrete literal.
The branches run top to bottom, and each one narrows what the later ones must handle: wide S first, wide SEP second, then the recursive match, then the empty-separator fixup. Here's a concrete failure if you reorder: check the pattern before string extends SEP, and Split<'a b c'> (where SEP defaults to the wide string) would match `${infer Left}${string}${infer Rest}` and start slicing the input at arbitrary points instead of returning the honest ['a b c']. Type-level programs are just as sensitive to guard ordering as runtime ones.
This challenge is originally from here.