Split
Split
Challenge
Implement a type-level Split that replicates JavaScript's String.prototype.split() in the type system.
type result = Split<'Hi! How are you?', ' '>
// expected: ['Hi!', 'How', 'are', 'you?']Solution
type Split<
S extends string,
SEP extends string
> = string extends S
? string[]
: S extends `${infer Head}${SEP}${infer Tail}`
? [Head, ...Split<Tail, SEP>]
: SEP extends ''
? []
: [S]The key idea: recursively match the separator in the string, accumulate the parts before it into a tuple, and handle edge cases for empty separators and generic string.
Breaking it down
Step 1 — Handle generic string type
string extends S ? string[] : ...If S is the broad string type (not a literal), we can't split it at the type level. Return string[] to match runtime behavior.
Step 2 — Match and split at separator
S extends `${infer Head}${SEP}${infer Tail}`
? [Head, ...Split<Tail, SEP>]
: ...Template literal inference finds the first occurrence of SEP in S:
Head= everything before the separatorTail= everything after the separator
We put Head in the tuple and recurse on Tail.
Step 3 — Base cases
SEP extends '' ? [] : [S]When there's no more separator match:
- If
SEPis''(empty string): return[]because the last character was already consumed - Otherwise:
Sis the final segment — return[S]
Walkthrough:
Split<'Hi! How are you?', ' '>
Round 1: Head = 'Hi!', Tail = 'How are you?'
→ ['Hi!', ...Split<'How are you?', ' '>]
Round 2: Head = 'How', Tail = 'are you?'
→ ['How', ...Split<'are you?', ' '>]
Round 3: Head = 'are', Tail = 'you?'
→ ['are', ...Split<'you?', ' '>]
Round 4: 'you?' doesn't contain ' '
→ ['you?']
Unwind: ['Hi!', 'How', 'are', 'you?'] ✓Deep Dive
Empty string separator
When SEP = '', the split should produce individual characters, like 'abc'.split('') → ['a', 'b', 'c']:
Split<'abc', ''>
Round 1: 'abc' extends `${infer Head}${''}${infer Tail}`
Head = '', Tail = 'abc'? No — TypeScript is greedy-minimal here.
Actually: Head = 'a', Tail = 'bc' (minimal non-empty match for Head)
Wait — with empty SEP, `${infer Head}${SEP}${infer Tail}` = `${infer Head}${infer Tail}`.
TypeScript infers: Head = 'a', Tail = 'bc'
→ ['a', ...Split<'bc', ''>]
Round 2: Head = 'b', Tail = 'c'
→ ['b', ...Split<'c', ''>]
Round 3: Head = 'c', Tail = ''
→ ['c', ...Split<'', ''>]
Round 4: '' extends `${infer Head}${infer Tail}`? No (can't split empty string)
→ SEP extends ''? Yes → []
Final: ['a', 'b', 'c'] ✓The SEP extends '' ? [] base case is specifically designed for this — when the empty-separator recursion reaches an empty string.
Multi-character separators
type T = Split<'a---b---c', '---'>
// ['a', 'b', 'c']Template literal matching treats SEP as a complete substring, not character-by-character.
Consecutive separators
type T = Split<'a,,b,,c', ','>
// ['a', '', 'b', '', 'c']This matches JavaScript behavior: consecutive separators produce empty strings.
Round 1: Head = 'a', Tail = ',b,,c'
Round 2: Head = '', Tail = 'b,,c'
Round 3: Head = 'b', Tail = ',c'
Round 4: Head = '', Tail = 'c'
Round 5: 'c' → [c]
→ ['a', '', 'b', '', 'c'] ✓Separator not found
type T = Split<'hello', ','>
// ['hello']When S doesn't contain SEP, the pattern match fails, and we return [S] — the entire string as a single-element tuple.
Why string extends S instead of S extends string?
S extends string is always true (due to the constraint). We need to detect if S is the broad string type vs. a string literal:
string extends 'hello' // false (string is wider than 'hello')
string extends string // trueSo string extends S is true only when S is string itself (or wider), not when it's a specific literal.
Comparison with runtime split()
| Behavior | Runtime | Type-level |
|---|---|---|
'a,b'.split(',') | ['a', 'b'] | ['a', 'b'] ✓ |
'abc'.split('') | ['a', 'b', 'c'] | ['a', 'b', 'c'] ✓ |
'a,,b'.split(',') | ['a', '', 'b'] | ['a', '', 'b'] ✓ |
'hello'.split(',') | ['hello'] | ['hello'] ✓ |
''.split(',') | [''] | [''] ✓ |
Key Takeaways
- Template literal inference naturally finds the first occurrence of a substring — perfect for implementing
split - Recursive tuple building with
[Head, ...Split<Tail, SEP>]mirrors how you'd implement split imperatively - Edge case handling (empty separator, generic
string, no match) requires careful base cases string extends Sis a reliable check for whetherSis the broadstringtype vs. a literal- This pattern demonstrates that TypeScript's type system can replicate non-trivial string operations
