What string bounds are
Every character in a string has a position, called an index. Indexes start at 0, so a string of length n has valid indexes 0 through n - 1. The first character is at 0 and the last one is at length - 1. Any index outside that range is out of bounds.
let car: string = "ford";
console.log(car.length); // 4
console.log(car[0]); // f
console.log(car[3]); // d
Here the length is 4, but the last valid index is 3. That difference of one is where most bounds mistakes come from.
What happens when you go past the end
TypeScript compiles to JavaScript, and reading a string index that doesn't exist doesn't stop the program. It gives you undefined:
let car: string = "ford";
console.log(car[4]); // undefined
console.log(car[30]); // undefined
console.log(car[-1]); // undefined
Negative indexes are out of bounds too. Square brackets don't count from the end the way they do in some other languages, so car[-1] is not the last character.
Many languages stop the program with an error on a bad index, which points you straight at the mistake. JavaScript and TypeScript stay quiet and hand back undefined, so the mistake shows up later, wherever that value is used.
The type system doesn't warn you
The declared type of car[4] is string, the same as the type of car[0]. Types are erased when the code is compiled, and the compiler doesn't know how long a string will be at run time. So this compiles without any error, and at run time ch holds undefined even though its type says string:
let car: string = "ford";
let ch: string = car[10];
console.log(ch); // undefined
console.log(ch.toUpperCase()); // TypeError: Cannot read properties of undefined (reading 'toUpperCase')
The second console.log crashes, and the error message talks about toUpperCase, not about the bad index. The real bug is one line earlier, which is why bounds mistakes can be slow to track down.
The exercise
The starting code should print the last character of message, which is s, but it prints undefined:
let message: string = "I love pancakes";
console.log(message[message.length]);
The string "I love pancakes" has 15 characters, counting the spaces. Its indexes run from 0 to 14, and message.length is 15, which is one past the last character. So message[message.length] is out of bounds and gives undefined.
Fixing it takes three small steps:
- Spot that the index inside the brackets is
message.length, the number of characters, not the position of the last one. - Subtract 1, because the last valid index is always one less than the length.
- Run it again and check that it prints
s.
The fixed code:
let message: string = "I love pancakes";
console.log(message[message.length - 1]);
Now the index is 14, which is a valid position, and the program prints s.
Common mistakes and how to avoid them
- Using
lengthas an index.s[s.length]is always out of bounds. The last character iss[s.length - 1]. - Looping one step too far. A loop written as
for (let i = 0; i <= s.length; i++)reachesi = s.lengthon its last turn and readsundefined. Usei < s.length. - Taking the last character of an empty string. For
"", the length is 0, sos[s.length - 1]iss[-1], which isundefined. If the string can be empty, checks.lengthbefore you index it. - Expecting negative indexes to count from the end. Use
s[s.length - 1], ors.at(-1), which does accept negative numbers. - Using the result without checking it. When an index comes from a calculation or from user input, test it against
s.lengthfirst.
A bounds check is a short comparison:
let message: string = "I love pancakes";
let i: number = 20;
if (i >= 0 && i < message.length) {
console.log(message[i]);
} else {
console.log("index out of range");
}
This prints index out of range, because 20 is not smaller than 15.
Key takeaways
- Valid indexes for a string run from
0tolength - 1. - Reading an index outside that range gives
undefinedinstead of an error. - The compiler can't catch this: the expression still has the type
string. - The last character is
s[s.length - 1], and it doesn't exist for an empty string. - Check an index against
s.lengthwhenever you aren't sure where it came from.