Start learning free

LESSON 66 OF 348 · STRINGS I

Exceeding String Bounds in TypeScript

Lesson Read the full explanation

When accessing string characters with indices, the most common problem we can run into is exceeding the string's bounds.

Remember, the characters of a string are normally indexed from 0 to length - 1.

Any index which is strictly greater than length - 1 is invalid.

When you try to access a character with an invalid index, TypeScript returns undefined:

let car = "ford";

// Valid indices: 0, 1, 2, 3
console.log(car[0]); // Output: f
console.log(car[3]); // Output: d

// Invalid indices: 4, 30
console.log(car[4]); // Output: undefined
console.log(car[30]); // Output: undefined

Some languages like Python wouldn't even let this code run, but throw an IndexError. JavaScript is more permissive, which is not necessary a good thing.

As a programmer, I'd rather be told when I made a mistake early on so I can fix it, instead of running into problems later on and not knowing what the root of those problems is.

In conclusion, we always want to double check our code to make sure that we don't exceed one string's bounds in our programs.


Assignment
Follow the Coding Tutorial and let's practice with string bounds!


Hint
Look at the examples above if you get stuck.


STEP 1 OF 1
    Ctrl + Enter to check

    TYPESCRIPT · STRINGS I

    Exceeding String Bounds in TypeScript, explained

    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:

    1. Spot that the index inside the brackets is message.length, the number of characters, not the position of the last one.
    2. Subtract 1, because the last valid index is always one less than the length.
    3. 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 length as an index. s[s.length] is always out of bounds. The last character is s[s.length - 1].
    • Looping one step too far. A loop written as for (let i = 0; i <= s.length; i++) reaches i = s.length on its last turn and reads undefined. Use i < s.length.
    • Taking the last character of an empty string. For "", the length is 0, so s[s.length - 1] is s[-1], which is undefined. If the string can be empty, check s.length before you index it.
    • Expecting negative indexes to count from the end. Use s[s.length - 1], or s.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.length first.

    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 0 to length - 1.
    • Reading an index outside that range gives undefined instead 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.length whenever you aren't sure where it came from.
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free