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?