A string is opaque to the type system until you can take it apart. Template literal types, added in TypeScript 4.1, are the tool that does it. Combined with infer, they turn pattern matching on string types into something close to a real parser. The bracket validator streams characters off its input one at a time with this technique. The Wordle engine reads guesses letter by letter the same way.

This article is about the matching rules underneath. They are subtler than they look.

Splitting off the first character

The foundational move pulls the head of a string away from its tail:

type Head<S extends string> =
  S extends `${infer H}${infer _Rest}` ? H : never;

type X = Head<'wordle'>;   // 'w'

Why does H get exactly one character and not the whole string? When two infer placeholders sit adjacent with nothing between them, TypeScript makes the first one as small as possible, a single character, and lets the second absorb the rest. That asymmetry is the entire reason you can walk a string from the left:

type ToChars<S extends string, Acc extends string[] = []> =
  S extends `${infer H}${infer Rest}`
    ? ToChars<Rest, [...Acc, H]>
    : Acc;

type C = ToChars<'abc'>;   // ['a', 'b', 'c']

Each pass peels one character into the accumulator and recurses on the rest, the loop shape from the recursion article. When S is finally '', it no longer matches `${infer H}${infer Rest}`. An empty string has no first character, so the conditional falls through to the base case and returns what it gathered.

Matching against a fixed delimiter

The single-character split is greedy-from-the-left only because nothing anchors it. Put a literal between the placeholders and the behaviour changes. Inference matches up to the first occurrence of that delimiter.

type BeforeDot<S extends string> =
  S extends `${infer Before}.${infer After}` ? Before : S;

type A = BeforeDot<'a.b.c'>;   // 'a'   stops at the FIRST dot

Before captures 'a', not 'a.b', because the compiler commits to the earliest delimiter it can find. That leftmost-match rule is what makes a splitter possible:

type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Tail}`
    ? [Head, ...Split<Tail, D>]
    : [S];

type Parts = Split<'2024-03-04', '-'>;   // ['2024', '03', '04']

Each step bites off everything up to the first delimiter, then recurses on the remainder. Swap the delimiter and the same type tokenizes paths ('/'), CSV rows (','), or namespaced keys ('.'). One eight-line type covers all of them.

When matching turns non-greedy

There is a wrinkle worth knowing before it bites you. The position of a placeholder relative to a literal flips its greediness:

// Looks like it should grab from the LAST delimiter, but it stops at the first.
type AfterFirstDot<S extends string> =
  S extends `${infer _Before}.${infer After}` ? After : never;

You might expect After to be greedy and grab from the last dot onward. But Before wins the leftmost match, so After gets everything past the first dot: 'b.c' for 'a.b.c'. To split on the last delimiter you have to recurse and keep the final segment, or pattern-match more deliberately. A placeholder bounded on its left by a literal is pinned to where that literal first appears, and the leftover placeholder soaks up the rest. Sketch the match on paper for anything non-trivial. The rules are consistent, but not always the ones intuition suggests.

Rebuilding, not just tearing apart

Template literals compose in both directions. The same syntax that destructures a string also constructs one, which is how the experiments render their output as readable text:

type Join<T extends string[], D extends string = ''> =
  T extends [infer Head extends string, ...infer Tail extends string[]]
    ? Tail extends []
      ? Head
      : `${Head}${D}${Join<Tail, D>}`
    : '';

type Row = Join<['VALID', '👍'], ' '>;   // 'VALID 👍'

The infer Head extends string constraint inside the tuple pattern is the modern touch. It narrows each element to string as the type extracts it, so the template literal on the next line accepts it without a separate cast. That is why Valid Parentheses can resolve to a clean 'VALID 👍' literal and Wordle can assemble an ASCII board. The parser reads characters in, and template construction writes the result back out.

Why this is the parser

Put the pieces together and you have the full toolkit a parser needs: read one token (`${infer H}${infer Rest}`), match a delimiter (`${infer A}${D}${infer B}`), branch on what you found (a conditional type), and accumulate the result (a tuple or a rebuilt string). The anagram experiment leans on all four to break a word into characters, count them, and reassemble palindromes from the pieces.

No string method runs. The compiler matches these patterns while it type-checks, and the answer is a literal type sitting in your editor before the program is ever built.

Next: the data structure all of this state lives in, tuples, and the arithmetic you can do with their length.