In this chapter, we're going to explore JavaScript programming styles and how developers worked with types in JavaScript (rather than JS++). This chapter will help you understand the next chapters which explain the JS++ type system in detail.
In this tutorial, we will be using the Google Chrome web browser. Click here to download Google Chrome if you don't already have it.
In order to execute JavaScript code, we'll be using the Chrome Developer Tools console. Open Chrome and hit the Ctrl + Shift + J key combination and choose the "Console" tab.
Copy and paste the following code into your console and press enter to execute it:
var message; message = "This is a test."; if (Math.random() > 0.5) { message = 123; } console.log(message);
Hit your "up arrow" and hit "enter" to evaluate the code more than once. Try evaluating the code a few times.
Notice how the data type in the above code changes from a string to a number. However, it only changes to a number if a randomly-generated number is greater than 0.5. Therefore, the data type of the variable 'message' can be different each time the script is executed. This was a major problem in JavaScript. For example, the following JavaScript code is unsafe:
function lowercaseCompare(a, b) { return a.toLowerCase() == b.toLowerCase(); }
The reason is because toLowerCase() is a method that's only available to JavaScript strings. Let's execute the following JavaScript code in the Chrome console:
function lowercaseCompare(a, b) { return a.toLowerCase() == b.toLowerCase(); } console.log("First message."); lowercaseCompare("10", 10); // Crashes with 'TypeError' console.log("Second message."); // Never executes.
Notice how the script will crash with a TypeError. The second message never gets logged. The key takeaway is that the code crashed because toLowerCase() is not a method available for numbers, but the function was called with a string ("10") and a number (10). The number argument was not a valid argument for the 'lowercaseCompare' function. If you change the function call, you will observe that the program no longer crashes:
// Change this: // lowercaseCompare("10", 10); // Crashes with 'TypeError' // to: lowercaseCompare("10", "10");
Developers worked around these problems in JavaScript by checking the types first. This is the safer way to rewrite the above 'lowercaseCompare' function in JavaScript:
function lowercaseCompare(a, b) { if (typeof a != "string" || typeof b != "string") { return false; } return a.toLowerCase() == b.toLowerCase(); }
We check the types using 'typeof', and, if we receive invalid argument types, we return a default value. However, for larger programs, this can result in a lot of extra code and there may not always be an applicable default value.
In the previous example, we explored one type of unforgiving error in JavaScript: a TypeError that causes script execution to end. There are many other types of errors that JS++ prevents, but, for now, we'll only look at one other category of errors: ReferenceErrors. What's wrong with the next bit of JavaScript code?
var message = "This is a test."; console.log(messag);
Try executing the above code in your console. Once again, nothing gets logged. Instead, you get a ReferenceError. This is because there's a typo in the above code. If we fix the typo, the code succeeds:
var message = "This is a test."; console.log(message);
JavaScript can fail on typos! TypeErrors and ReferenceErrors don't happen in JS++. We classify TypeErrors and ReferenceErrors as "unforgiving" errors because they can cause JavaScript script execution to halt. However, there's another type of error in JavaScript that's a little more dangerous because they're "silent."
There is a class of "silent" errors in JavaScript that can silently continue to propagate through your program. We call these "forgiving" errors because they don't stop script execution, but, despite the innocuous name, we can consider them more dangerous than unforgiving errors because they continue to propagate.
Consider the following JavaScript function:
function subtract(a, b) { return a - b; }
This function might seem straightforward on the surface, but – as the script gets more complex – when variables change and depend on other values spanning thousands of lines of code, you might end up accidentally subtracting a variable that ends up being a number from a variable that ends up being a string. If you attempt such a call, you will get NaN (Not a Number).
Evaluate the following code in your console:
function subtract(a, b) { return a - b; } subtract("a", 1);
Observe the resulting NaN (Not a Number) value. It doesn't crash your application so we call it a forgiving error, but the error value will propagate throughout the rest of your program so your program continues to silently run with errors. For example, subsequent calculations might depend on the value returned from the 'subtract' function. Let's try additional arithmetic operations to observe:
function subtract(a, b) { return a - b; } var result = subtract("a", 1); // NaN console.log(result); result += 10; // Add 10 to NaN console.log(result);
No crash and no error reports. It just silently continues to run with the error value.
You won't be able to run the following code, but here's an illustration of how such error values might propagate through your application in a potential real-world scenario, a shopping cart backend:
var total = 0; total += totalCartItems(); while ((removedPrice = removedFromCart()) != null) { total = subtract(total, removedPrice); } total += tax(); total += shipping();
In the example above, our shopping cart can end up with a NaN (Not a Number) value – resulting in lost sales for the business that can be difficult to detect because there were no explicit errors.
JS++ was designed based on extensive JavaScript development experience – not just for large, complex applications but anywhere JavaScript could be used – scripts and macros for Windows Script Host to legacy programs based on ActiveX and the like which are still prevalent in some corporate environments. In short, JS++ will work anywhere that JavaScript is expected – from the basic to the complex to the arcane.
One important observation relevant to JS++ is that most JavaScript programs are already well-typed (but not "perfectly" typed). Recall the "unsafe" and "safe" versions of the JavaScript 'lowercaseCompare' function:
// Unsafe: function lowercaseCompare(a, b) { return a.toLowerCase() == b.toLowerCase(); } // Safe: function lowercaseCompare(a, b) { if (typeof a != "string" || typeof b != "string") { return false; } return a.toLowerCase() == b.toLowerCase(); }
The safe version is much more tedious, and – in practice – most JavaScript developers will write most of their functions the unsafe way. The reason is because, by looking at the function body, we know the expected parameter types are strings because both parameters use the 'toLowerCase' method only available to strings. In other words, in JavaScript, we have an intuition about the types just by looking at the code.
Consider the following variables and guess their types:
var employeeAge; var employeeName; var isEmployed;
employeeAge
makes sense as a number, employeeName
makes sense as a string, and isEmployed
makes sense as a Boolean.
Now try guessing the expected parameter types for the following functions:
function multiply(a, b) { return a * b; } function log(message) { console.log("MESSAGE: " + message); }
The function 'multiply' makes most sense if you supply numeric arguments to the 'a' and 'b' parameters. Furthermore, the 'log' function is most correct with strings.
Sometimes, instead of checking the type using 'typeof', JavaScript programmers will instead force a conversion of the argument to the data type they need (especially if intuition might fail). This technique is an instance of type coercion and results in code that is more fault tolerant because it won't exit with an exception if the data type of the argument provided is incorrect.
Once again, let's see how we can change our 'lowercaseCompare' example using this idea:
// Unsafe: function lowercaseCompare(a, b) { return a.toLowerCase() == b.toLowerCase(); } // Safer: function lowercaseCompare(a, b) { a = a.toString(); b = b.toString(); return a.toLowerCase() == b.toLowerCase(); }
In the re-written version of the 'lowercaseCompare' function, we are "forcing" the 'a' and 'b' arguments to be converted to a string. This allows us to safely call the 'toLowerCase' method without a crash. Now, if the 'lowercaseCompare' function is called, we get the following results:
lowercaseCompare("abc", "abc") // true lowercaseCompare("abc", 10) // false lowercaseCompare("10", "10") // true lowercaseCompare("10", 10) // true
However, the astute observer will notice the new version of 'lowercaseCompare' is marked "safer" rather than "safe."
Why?
toString is not the most correct way to force a conversion to a string. (It's also not the fastest due to runtime method lookups, but imagine having to consider all these details while writing one line of code? This is how programming for the web used to be before JS++.)
One example is if we try to call 'lowercaseCompare' with a variable we forgot to initialize, it will crash again if we use 'toString'. Let's try it:
function lowercaseCompare(a, b) { a = a.toString(); b = b.toString(); return a.toLowerCase() == b.toLowerCase(); } var a, b; // uninitialized variables var result = lowercaseCompare(a, b); console.log(result); // Never executes
No, instead, the most correct way to perform type coercion to string would be like this:
// Finally safe: function lowercaseCompare(a, b) { a += ""; // correct type coercion b += ""; // correct type coercion return a.toLowerCase() == b.toLowerCase(); } var a, b; var result = lowercaseCompare(a, b); console.log(result);
There's just one problem left with the correct code: it becomes unreadable. What would your code look like if you had to insert += "" everywhere that you wish to express the intent that you want string data?
Now that was a lot to digest! Writing good code in JavaScript is hard. Imagine having to take into account all these considerations when writing a small bit of code in JavaScript: safety, performance, code readability, unforgiving errors, silent errors, correctness, and more. This actually only scratches the surface of JavaScript corner cases, but it provides us enough information to begin understanding types in JS++.
However, if we write our code in JS++, JS++ actually handles all these considerations for us. This means you can write code that is readable, but the JS++ compiler will handle generating code that is fast, safe, and correct.
Before we move on to the next chapter – which explains the JS++ type system in detail – let's try to rewrite the 'lowercaseCompare' code in JS++. We'll start with code that is intentionally incorrect to show you how JS++ catches such errors early and show you how to fix them. Create a 'test.jspp' file and type in the following code:
import System; function lowercaseCompare(string a, string b) { return a.toLowerCase() == b.toLowerCase(); } Console.log("First message."); lowercaseCompare("10", 10); Console.log("Second message.");
Try compiling the file. It won't work. JS++ found the error early:
[ ERROR ] JSPPE5024: No overload for `lowercaseCompare' matching signature `lowercaseCompare(string, int)' at line 8 char 0 at test.jspp
It tells you exactly the line where the error occurred so you can fix it – before your users, visitors, or customers get a chance to encounter it. Let's fix the offending line, which JS++ told us was on Line 8:
// lowercaseCompare("10", 10); // becomes: lowercaseCompare("10", "10");
Run the code after fixing the offending line. In Windows, right-click the file and choose "Execute with JS++". In Mac or Linux, run the following command in your terminal:
> js++ --execute test.jspp
You'll see both messages logged successfully.
In the next chapter, we'll explore the JS++ type system and "type guarantees" by example.