Yuzhe's Blog

yuzhes

Length of String

Length of String

Challenge Link

Challenge

Compute the length of a string literal, which behaves like String#length.

type T0 = LengthOfString<"hello">   // 5
type T1 = LengthOfString<"">        // 0
type T2 = LengthOfString<"typescript"> // 10

Solution

type LengthOfString<
  S extends string,
  T extends string[] = []
> = S extends `${infer F}${infer R}`
  ? LengthOfString<R, [...T, F]>
  : T['length']

The core idea: TypeScript can’t read .length off a string literal type directly — but it can read .length off a tuple. So we convert the string, one character at a time, into a tuple, then return T['length'].

Breaking it down

Step 1 — Template literal infer split

S extends `${infer F}${infer R}`

When S = "hello", TypeScript infers F = "h" and R = "ello". The first character is peeled off, and the tail is left for recursion. When S = "", the pattern fails to match and we fall through to the base case.

Step 2 — Accumulate into a tuple

LengthOfString<R, [...T, F]>

Each character is appended to the accumulator T. After processing "hello":

Step 3 — Read the length

When S = "", the conditional fails and we return T['length']. Tuple length is a numeric literal type, so ["h","e","l","l","o"]['length'] evaluates to 5.

Deep Dive

Why can’t we just use S['length']?

In JavaScript, "hello".length === 5. But in TypeScript’s type system:

type L = "hello"['length'] // string (not 5!)

TypeScript knows strings have a .length, but it’s typed as number (or string in some contexts) — it doesn’t know the value at the type level. The type system simply doesn’t track string character count natively.

Tuples, on the other hand, do carry their length as a numeric literal:

type L = ["h","e","l","l","o"]['length'] // 5 ✅

That’s because tuple types are structurally exact — TypeScript knows ["h","e","l","l","o"] has exactly 5 elements.

The accumulator pattern

The T extends string[] = [] parameter is a classic accumulator pattern in recursive type utilities. Instead of building the result on the way back up from recursion, we carry the state forward:

// ❌ Without accumulator — would need to add 1 somehow on return
type Bad<S extends string> = S extends `${infer F}${infer R}`
  ? 1 + Bad<R>  // TypeScript can't do arithmetic like this
  : 0

// ✅ With accumulator — length falls out naturally from tuple
type Good<S extends string, T extends string[] = []> = 
  S extends `${infer F}${infer R}`
    ? Good<R, [...T, F]>
    : T['length']

This pattern avoids needing arithmetic on types, which TypeScript doesn’t support directly. The tuple itself acts as a counter.

Recursion depth

TypeScript has a recursion depth limit (~1000 levels). Since each character = one recursive step, this approach works for strings up to ~1000 characters long. For real-world string literals this is rarely a concern.

How infer F matches a single character

You might wonder: when we write `${infer F}${infer R}`, why does F get just one character instead of, say, the first half?

TypeScript uses a greedy-then-backtrack strategy. The inferred R is resolved to the shortest possible string that satisfies the pattern (greedy F, minimal R). For a two-infer template literal with no separator, the split is F = first char, R = rest. This is guaranteed behavior for the leading infer in a template literal.

Key Takeaways