Annotated solution
Published August 2, 2026The solution
type Join<T extends readonly string[], D extends string> = T extends readonly [infer F extends string, ...infer R extends string[]] ? R extends readonly [] ? F : `${F}${D}${Join<R, D>}` : ''
The common wrong answer
type Join<T extends readonly string[], D extends string> = T extends readonly [infer F extends string, ...infer R extends string[]] ? `${F}${D}${Join<R, D>}` : ''
Every level appends a delimiter, including the last one, so `["a","b","c"]` joins to `"a-b-c-"`. Joining is not "put a separator after each item" — it is "put a separator *between* items", and the difference only shows at the end.
Line by line
infer F extends stringThe `extends string` constraint on an `infer` narrows what is bound. Without it `F` would be `unknown`, which cannot be interpolated into a template literal type.
R extends readonly [] ? FThe guard that makes it a join rather than a suffix. When nothing follows, the element is emitted bare and the recursion stops without contributing a delimiter.
Takeaway
Recursive string building almost always needs a distinct base case for the final element. If your output has a stray delimiter on the end, that case is missing.