Count the length of a string type with a tuple accumulator. The naive recursion dies at about 45 characters; tail recursion takes you to 999.
Counting the characters of a string type is easy until the string is 999 characters long and the compiler's recursion limit gets in the way.
LengthOfString<S> calculates the length of a template string, the same task as 298 - Length of String:
[object Object]The twist: the type must support strings several hundred characters long. The usual recursive calculation of string length is limited by TypeScript's recursion depth and gives up at around 45 characters. So this challenge is really about writing recursive types that scale, a technique you'll reuse in every serious type-level algorithm.
Implement a type LengthOfString<S> that calculates the length of the template string (as in 298 - Length of String):
[object Object]The type must support strings several hundred characters long (the usual recursive calculation of the string length is limited by the depth of recursive function calls in TS, that is, it supports strings up to about 45 characters long).
View on GitHub: https://tsch.js.org/651
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.
The whole solution fits in five lines:
type LengthOfString<
S extends string,
Acc extends string[] = [],
> = S extends `${infer First}${infer Rest}`
? LengthOfString<Rest, [...Acc, First]>
: Acc['length']The code is short, but the shape of the recursion is the entire point.
At the type level, a string literal like 'foo' has no length property with a useful literal value; 'foo'['length'] is just number. Tuples are different: ['f', 'o', 'o']['length'] is exactly 3. So the strategy is to convert the string into a tuple, one element per character, then read the tuple's length.
The pattern `${infer First}${infer Rest}` uses a key rule of template literal inference: when two infer placeholders are adjacent, the first one matches exactly one character and the rest goes to the second. For S = 'foo':
// First = 'f'
// Rest = 'oo'The empty string '' doesn't match this pattern at all, since there is no first character to grab. That is what eventually stops the recursion, and also what makes LengthOfString<''> evaluate to 0 without any special case.
Your first instinct might be to recurse and add one at each step, building the tuple on the way back up:
type NaiveTuple<S extends string> = S extends `${infer F}${infer R}`
? [F, ...NaiveTuple<R>]
: []
type NaiveLength<S extends string> = NaiveTuple<S>['length']The problem sits in [F, ...NaiveTuple<R>]: after the recursive call returns, there's still work left to do (spreading the result into a new tuple). The outer type can't finish until the inner recursive call finishes, so the compiler has to keep every level of the recursion on its internal stack. TypeScript caps that depth, and around 45 to 50 nested instantiations it errors with 'Type instantiation is excessively deep and possibly infinite'. The 272- and 999-character test cases are hopeless this way.
The real solution threads an accumulator Acc through the recursion. Each step appends the character it just peeled off, [...Acc, First], and then the recursive call LengthOfString<Rest, [...Acc, First]> is the entire result of the branch. Nothing is left to do after the call returns.
That last property is called tail recursion, and TypeScript (since 4.5) detects it and evaluates the recursion as a loop instead of a nested stack of instantiations. The depth limit for tail-recursive conditional types is 1000 iterations instead of about 50. That is why the 999-character test case in this challenge passes, and why it's 999 rather than a round 1000.
Tracing a small input:
LengthOfString<'foo'>
// β LengthOfString<'oo', ['f']>
// β LengthOfString<'o', ['f', 'o']>
// β LengthOfString<'', ['f', 'o', 'o']>
// β ['f', 'o', 'o']['length']
// β 3When S no longer matches the pattern (it's empty), we return Acc['length'], the count of characters we collected.
LengthOfString<''> β 0: the empty string fails the pattern immediately, so the default accumulator [] reports length 0.The accumulator-plus-tail-recursion pattern is one of the most important idioms in advanced TypeScript. Whenever a recursive type dies with 'excessively deep', ask whether you can move the work into the arguments so the recursive call is the last thing that happens.
This challenge is originally from here.