Yuzhe's Blog

yuzhes

Split

Split

Challenge Link

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:

We put Head in the tuple and recurse on Tail.

Step 3 — Base cases

SEP extends '' ? [] : [S]

When there’s no more separator match:

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   // true

So string extends S is true only when S is string itself (or wider), not when it’s a specific literal.

Comparison with runtime split()

BehaviorRuntimeType-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