Get the exact string length in TypeScript
- typescript
How can we get the length of a string using TypeScript? Well, every string has a length
property, doesn’t it? And we can access it in TypeScript using the bracket notation. Let’s try!
const myString = "marshmallow" type Strlen<S extends string> = S["length"]; type MyStrlen = Strlen<myString> // "number" 🤔
Sadly, it doesn’t work the way we wanted. We got the number
type. Technically, there’s nothing wrong with that—the type of string length is indeed a number. But what about the exact number – 11 in the case of “marshmallow”?
Sidebar: What about arrays? Can TypeScript get the exact length of an array? Let’s try:
const myArray = ["chocolate", "liquorice", "marshmallow"] type Arrlen<A extends unknown[]> = A["length"] type MyArrlen<typeof myArray> // "number" 🤔
Still no success. Well, TypeScript actually can’t get the exact length of an array. The only data structure from which it can obtain length is a tuple, which is a list-like data structure with a fixed length. JavaScript currently doesn’t have a tuple data structure, but TypeScript kind of does—a readonly array.
const myTuple = ["chocolate", "liquorice", "marshmallow"] as const /* or myTuple: readonly string[] = ["chocolate", "liquorice", "marshmallow"] or myTuple = ReadonlyArray<string> = ["chocolate", "liquorice", "marshmallow"] */ type Arrlen<A extends readonly unknown[]> = A["length"] type MyArrlen<typeof myArray> // 3 🎉
In practice, using type assertions (as const
, read-only
) would be common, but there is an even simpler way to do this (and it is important to show this, because otherwise you may be confused about why the final code does not use type assertions):
type Arrlen<A extends unknown[]> = A["length"] // no readonly! type MyArrlen<["chocolate", "liquorice", "marshmallow"]> // 3 🎉
As we can see, if we use a type literal, TypeScript regards it as a tuple by default.
To get to the point, we can exactly determine the length of a string by converting it to a tuple of the same length.Is this possible? Good news: it is!
This might get a little scary for someone, but there is nothing too arcane, just a simple tail-call recursion, which is a substitute for a loop (TypeScript is currently very limited regarding loops—probably only the in
syntax can be considered a loop).
First, let’s create equivalent code in JavaScript:
function strlen(str, cache = []) { const first = str.slice(0, 1); const rest = str.slice(1); // the TS version is closer to `let [first, ...rest] = str`, // but it breaks in JS once `...rest` gets `undefined` (which is not iterable) return first.length ? strlen(rest, [...cache, first]) : cache.length; } console.log(strlen("marshmallow")) // 11 🎉
This is, of course, kind of ridiculous, because we can simply do "marshmallow".length
in JavaScript. But as has been said before, it doesn’t work the same way in TypeScript.
This is the TypeScript version of the “strlen” function:
type Strlen< Str extends string, Cache extends string[] = [] > = Str extends `${infer First}${infer Rest}` ? Strlen<Rest, [...Cache, First]> // <-- The array is a type literal, no need for 'readonly' (see above) : Cache["length"] type MyStrlen = Strlen<"marshmallow"> // 11 🎉
Heureka, we have our exact length!
Confused about what’s going on here? We’re recursively iterating over a string. At each iteration, we pull out the first character (using infer
) and add it to a Cache
tuple. Once we are done iterating (we are done when Rest
can’t be inferred, because there isn’t any character left), we return the length of the Cache
tuple.
👍 Enjoy!
If you find anything in this post that should be improved (either factually or in language), feel free to edit it on Github .