Length of String
Length of String
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"> // 10Solution
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 0:
T = [],S = "hello" - Step 1:
T = ["h"],S = "ello" - Step 2:
T = ["h","e"],S = "llo" - Step 3:
T = ["h","e","l"],S = "lo" - Step 4:
T = ["h","e","l","l"],S = "o" - Step 5:
T = ["h","e","l","l","o"],S = ""
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
- String literals don't expose numeric length — but tuples do via
T['length'] - Accumulator pattern: carry a growing tuple through recursion, then read its length at the base case
`${infer F}${infer R}`reliably peels off the first character of a string literal- This technique generalizes: wherever you need to "count" string characters, convert to a tuple first
