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?