🎓 A1Learner
🔍
0 of 37 lessons completed

TypeScript Template Literal Parsing

Template literal parsing reads a string literal into structured types such as tuples. The Split type consumes the literal one segment at a time and collects the pieces in order.

Split reads a literal into a tuple

The pattern binds the text before the first comma as Head and the remainder as Rest. The build repeats until one segment remains.

type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Rest}`
    ? [Head, ...Split<Rest, D>]
    : [S];
const parts: Split<'pen,pencil,marker', ','> = ['pen', 'pencil', 'marker'];
console.log(parts[0]);
console.log(parts[2]);
console.log(parts.length);

Join folds a tuple back into a literal

Join reads the first member and the remaining tuple apart. The delimiter is written between members until one member is left.

type Join<T extends string[], D extends string> =
  T extends [infer Head extends string, ...infer Rest extends string[]]
    ? Rest['length'] extends 0
      ? Head
      : `${Head}${D}${Join<Rest, D>}`
    : '';
const csvLine: Join<['pen', 'pencil', 'marker'], '-'> = 'pen-pencil-marker';
console.log(csvLine);

A route pattern gives up its parameter names

Splitting the route at each slash gives the segment tuple. A conditional keeps only the segments that start with a colon and strips the colon.

type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Rest}`
    ? [Head, ...Split<Rest, D>]
    : [S];
type ParamName<S extends string> = S extends `:${infer Param}` ? Param : never;
type RouteSegments = Split<'users/:id/posts/:postId', '/'>;
const paramList: ParamName<RouteSegments[number]>[] = ['id', 'postId'];
console.log(paramList);

A wrong tuple member is refused

The tuple type fixes every member literal, so a segment that was never in the string cannot be assigned at that position.

type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Rest}`
    ? [Head, ...Split<Rest, D>]
    : [S];
const badParts: Split<'a,b', ','> = ['a', 'c'];

Example

type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Rest}`
    ? [Head, ...Split<Rest, D>]
    : [S];
type Join<T extends string[], D extends string> =
  T extends [infer Head extends string, ...infer Rest extends string[]]
    ? Rest['length'] extends 0
      ? Head
      : `${Head}${D}${Join<Rest, D>}`
    : '';
type Row = Split<'red,green,blue', ','>;
const row: Row = ['red', 'green', 'blue'];
const joined: Join<Row, ' / '> = 'red / green / blue';
console.log(row[1]);
console.log(joined);

Key points

  • Split matches a head segment, the delimiter, and the rest of the literal in one pattern.
  • Each match adds one member to the front of the tuple and recurses on the rest.
  • The recursion stops when no delimiter remains, and the final segment becomes the last member.
  • Join runs the parse backwards, folding a tuple of literals into one string literal.
  • Tuple positions prove the parse, since each position holds one exact segment literal.
  • A route pattern parsed the same way gives up its parameter names as literal types.

In this example, Split parses the colour list into a tuple and Join folds that same tuple back into one literal with a new delimiter. The round trip proves both directions of the parse agree.

Try it Yourself

Edit the code, then press Run.

Running is simulated · TypeScript Playground

Exercise

What type does Split<'a-b-c', '-'>[1] hold?