#847β€’Hard

String Join

A curried join function whose return type is the exact joined string literal, computed from the delimiter and arguments by a recursive template literal type.

join('-')('a', 'b', 'c') should return the exact literal type 'a-b-c', not just string.

Most type challenges hand you a type alias to fill in. This one types a real function, and a curried one at that: the delimiter is captured by the outer call, the arguments are inferred as a tuple of string literals by the inner call, and a recursive template literal type folds them together. It's the same machinery that lets libraries compute exact route strings or class names at compile time.

The utility can be used like so:

const hyphenJoiner = join('-')
const result = hyphenJoiner('a', 'b', 'c'); // = 'a-b-c'

Or alternatively:

[object Object]

When we pass an empty delimiter (i.e '') to join, we should concat the strings as they are, i.e:

[object Object]

When only one item is passed, we should get back the original item (without any delimiter added):

[object Object]

Challenge Instructions: String Join

Hard

Create a type-safe string join utility which can be used like so:

const hyphenJoiner = join('-')
const result = hyphenJoiner('a', 'b', 'c'); // = 'a-b-c'

Or alternatively:

[object Object]

When we pass an empty delimiter (i.e '') to join, we should concat the strings as they are, i.e:

[object Object]

When only one item is passed, we should get back the original item (without any delimiter added):

[object Object]

View on GitHub: https://tsch.js.org/847

Change the following code to make the test cases pass (no type check errors).

πŸ‘‹ Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The full solution:

type Join<Parts extends string[], D extends string> =
  Parts extends [infer First extends string, ...infer Rest extends string[]]
    ? Rest extends []
      ? First
      : `${First}${D}${Join<Rest, D>}`
    : ''
 
declare function join<D extends string>(
  delimiter: D
): <Parts extends string[]>(...parts: Parts) => Join<Parts, D>

There are two halves: a function signature that captures literal types, and a recursive Join type that computes with them. The signature first.

Capturing the delimiter with an outer generic

join<D extends string>(delimiter: D) is the outer half of the curried API. Because D is a type parameter constrained to string (not annotated as plain string), TypeScript infers the literal type of the argument: calling join('-') gives D = '-', not D = string. That literal is then baked into the returned function's type. It's a closure at the type level, mirroring the closure you'd write at runtime.

Inferring the arguments as a literal tuple

The inner function is generic too: <Parts extends string[]>(...parts: Parts). Two inference behaviors combine here:

Without the generic, say (...parts: string[]), you'd get a widened string[] and the exact result type would be unrecoverable. Declaring Parts on the inner function (rather than alongside D) is also what makes the curried style work: it's inferred fresh on every call of hyphenJoiner.

Folding the tuple: the Join type

Join<Parts, D> walks the tuple recursively. The pattern

[object Object]

splits a tuple into its head and tail, with inline extends constraints on the infers so First and Rest are immediately usable as string types (no extra conditional needed). Then:

Tracing Join<['a', 'b', 'c'], '-'>:

// 'a' + '-' + Join<['b', 'c'], '-'>
// 'a' + '-' + ('b' + '-' + Join<['c'], '-'>)
// 'a' + '-' + ('b' + '-' + 'c')   β†’  'a-b-c'

A delimiter is only ever emitted between two parts. That's the classic join invariant, encoded by putting the separator inside the recursive branch rather than after every element.

Edge cases the tests cover

The takeaway pattern: pair literal-preserving generics on a function signature with a recursive tuple-folding type, and the compiler can compute exact result strings for you. Once you've seen it here, you'll recognize it in typed path.join implementations and query builders.

This challenge is originally from here.

Share this challenge

Learn the Concepts