JavaScript is loosely typed. It's worth repeating, JavaScript is loosely typed. Notice how the sentence is passive--we cannot categorically hold someone responsible for the loose-type nature of JavaScript just as we can't do so for other famous glitches of JavaScript.
A detailed discussion on what loose-types and loosely typed languages are will help aid your understanding of the problem that we plan to solve with this book.
When a programming language is loosely typed, it means that the data passed around using variables, functions, or whatever member applicable to the language does not have a defined type. A variable x could be declared, but the kind of data it holds is never certain. Loosely typed languages are contrary to strongly typed languages, which enforce that every declared member must strictly define what sort of data it can hold.
These types are categorized into:
- Strings
- Numbers (int, float, and so on.)
- Data structures (arrays, lists, objects, maps, and so on.)
- Boolean (true and false)
JavaScript, PHP, Perl, Ruby, and so on, are all examples of loosely typed languages. Java, C, C#, are examples of strongly typed languages.
In loosely typed languages, a member may be initially defined as a string. Down the line, this member could end up storing a number, a boolean, or even a data structure. This instability leads us to the implications of loosely typed languages.
Before we keep moving, it would be nice to define the common jargon you may have met or will meet with in the course of understanding loose and strict types:
- Members: These are the features of a language that describe how the data is stored and manipulated. Variables, functions, properties, classes, interfaces, and so on, are all examples of the possible members a language can have.
- Declared versus defined versus assigned: When a variable is initialized with no value, it is said to be declared. When it is declared and has a type, it is said to be defined. When the variable has a value, whether typed or not, it is assigned.
- Types: These are used to categorize the data based on how they are parsed and manipulated. For example, numbers, strings, booleans, arrays, and so on.
- Values: The data assigned to a given member is known as the member's value.
Let's start out with an example to show how loosely typed languages behave:
// Code 1.1 // Declare a variable and assign a value var x = "Hello"; // Down the line // you might have forgotten // about the original value of x // // // Re-assign the value x = 1; // Log value console.log(x); // 1
The variable x
was initially declared and assigned a string value, Hello
. The same x
got re-assigned to a numeric value, 1
. Nothing went wrong; the code was interpreted and when we logged the value to the console, it logged the latest value of x
, which is 1
.
This is not just a string-number thing; the same thing applies to every other type, including complex data structures:
// Code 1.2 var isCompleted; // Assign null isCompleted = null; console.log('When null:', isCompleted); // Re-assign a boolean isCompleted = false; console.log('When boolean:', isCompleted); // Re-assign a string isCompleted = 'Not Yet!'; console.log('When string:', isCompleted); // Re-assign a number isCompleted = 0; console.log('When number:', isCompleted); // Re-assign an array isCompleted = [false, true, 0]; console.log('When array:', isCompleted); // Re-assign an object isCompleted = {status: true, done: "no"}; console.log('When object:', isCompleted); /** * CONSOLE: * * When null: null * When boolean: false * When string: Not Yet! * When number: 0 * When array: [ false, true, 0 ] * When object: { status: true, done: 'no' } */
The important thing to note here is not that the values are changing. Rather, it's the fact that both values and types are changing. The change in the type does not affect the execution. Everything works fine, and we have our expected result in the console.
The function parameters and return types are not left out either. You can have a function signature that accepts a string parameter, but JavaScript will keep silent when you, or any other developer, pass in a number while calling the function:
function greetUser( username ) { return `Hi, ${username}` } console.log('Greet a user string: ', greetUser('Codebeast')) console.log('Greet a boolean: ', greetUser(true)) console.log('Greet a number: ', greetUser(1)) /** * CONSOLE: * * Greet a user string: Hi, Codebeast * Greet a boolean: Hi, true * Greet a number: Hi, 1 */
If you're coming from a strong-type background and have no previous experience with loosely typed languages, the preceding example must feel weird. This is because in strongly typed languages, it's hard to change the type of the particular member (variables, functions, and so on).
So, what is the implication to take note of? The obvious implication is that the members that are loosely typed are inconsistent. Therefore, their value types can change, and this is something that you, the developer, will need to watch out for. There are challenges in doing so; let's talk about them.
Loose types are tricky. At first glance, they appear to be all nice and flexible to work with--flexibility, as in giving you the freedom to change types anytime and anywhere, without the interpreter screaming errors like other strongly typed languages do. Just like any other form of freedom, this one also comes with a price.
The major problem is inconsistency. It is very easy to forget the original type for a member. This could lead you to handling, say, a string as if it were still a string when its value is now Boolean. Let's see an example:
function greetUser( username ) { // Reverse the username var reversed = username.split('').reverse().join(''); return `Hi, ${reversed}` } console.log('Greet a correct user: ', greetUser('Codebeast')) * CONSOLE: * * Greet a correct user: Hi, tsaebedoC */
In the preceding example, we have a function that greets the users based on their usernames. Before it does the greeting, it first reverses the username. We can call the function by passing in a username string.
What happens when we pass in a Boolean or some other type that does not have a split
method? Let's check it out:
// Code 1.4 function greetUser( username ) { var reversed = username.split('').reverse().join(''); return `Hi, ${reversed}` } console.log('Greet a correct user: ', greetUser('Codebeast')) // Pass in a value that doesn't support // the split method console.log('Greet a boolean: ',greetUser(true)) * CONSOLE: * * Greet a correct user: Hi, tsaebedoC * /$Path/Examples/chapter1/1.4.js:2 * var reversed = username.split('').reverse().join(''); ^ * TypeError: username.split is not a function */
The first log output, which prints the greeting with a string, comes out fine. But the second attempt fails because we passed in a Boolean. In as much as everything in JavaScript is an object, a Boolean does not have a split
method. The image ahead shows a clear output of the preceding example:
Yes, you might be thinking that you're the author of this code; why would you pass in a Boolean when you designed the function to receive a string? Remember that a majority of the code that we write in our lifetime is not maintained by us, but by our colleagues.
When another developer picks up greetUser
and decides to use the function as an API without digging the code's source or documentation, there is a high possibility that he/she won't pass in the right value type. This is because he/she is blind. Nothing tells him/her what is right and what is not. Even the name of the function is not obvious enough to make her pass in a string.
JavaScript evolved. This evolution was not just experienced internally but was also seen in its vast community. The community came up with best practices on tackling the challenges of the loose-type nature of JavaScript.
JavaScript does not have any native obvious solution to the problems that loose types bring to the table. Rather, we can use all forms of manual checks using JavaScript's conditions to see whether the value in question is still of the intended type.
We are going to have a look at some examples where manual checks are applied in order to retain the integrity of the value types.
The popular saying that Everything is an Object in JavaScript is not entirely true (https://blog.simpleblend.net/is-everything-in-javascript-an-object/). There are Objects and there are Primitives. Strings, numbers, Boolean, null, undefined, are primitives but are handled as objects only during computation. That's why you can call something like .trim()
on a string. Objects, arrays, dates, and regular expressions are valid objects. It's mind-troubling to say that an object is an object, but that is JavaScript for you.
The typeof
operator is used to check the type of a given operand. You can use the operator to control the harm of loose types. Let's see some examples:
// Code 1.5 function greetUser( username ) { if(typeof username !== 'string') { throw new Error('Invalid type passed'); }; var reversed = username.split('').reverse().join(''); return `Hi, ${reversed}` } console.log('Greet a correct user: ', greetUser('Codebeast')) console.log('Greet a boolean: ',greetUser(true))
Rather than waiting for the system to tell us that we're wrong when an invalid type is passed in, we catch the error as early as possible and throw a custom and more friendly error, as shown in the following screenshot:
The typeof
operator returns a string, which represents the value's type. The typeof
operator is not entirely perfect and should only be used when you are sure about how it works. See the following issue:
function greetUser( user ) { if ( typeof user !== 'object' ) { throw new Error('Type is not an object'); } return `Hi, ${user.name}`; } console.log('Greet a correct user: ', greetUser( {name: 'Codebeast', age: 24 } )) // Greet a correct user: Hi, Codebeast console.log('Greet a boolean: ', greetUser( [1, 2, 3] )) // Greet a boolean: Hi, undefined
You may have expected an error to be thrown when the function was called with an array for the second time. Instead, the program got past the check and executed user.name
before realizing that it is undefined. Why did it get past this check? Remember that an array is an object. Therefore, we need something more specific to catch the check. Date and regex could have passed the check as well, even though that may not have been the intent.
The toString
method is prototypically inherited by all the objects and wrapped objects (primitives). When you call this method on them, it returns a string token of the type. See the following examples:
Object.prototype.toString.call([]);// [object Array]Object.prototype.toString.call({});// [object Object]Object.prototype.toString.call('');// [object String]Object.prototype.toString.call(newDate());// [object Date] // etc
Now you can use this to check the types, as shown by Todd Motto (https://toddmotto.com/understanding-javascript-types-and-reliable-type-checking/#true-object-types):
var getType = function (elem) { return Object.prototype.toString.call(elem).slice(8, -1); }; var isObject = function (elem) { return getType(elem) === 'Object'; }; // You can use the function // to check types if (isObject(person)) { person.getName(); }
What the preceding example does is check the part of the string returned by the toString
method to determine its type.
The examples we saw previously are just an overkill for a simple type check. If JavaScript had strict type features, we wouldn't have gone through this stress. In fact, this chapter would never have existed.
Imagine that JavaScript could do this:
function greet( username: string ) { return `Hi, ${username}`; }
We wouldn't have gone through all that type checking hell because the compiler (as well as the editors) would have thrown errors when it encountered type inconsistency.
This is where TypeScript comes in. Luckily, with TypeScript, we can write code that looks like the preceding one, and we can have it transpiled to JavaScript.
Throughout this book, we will be talking about TypeScript for building not just JavaScript apps but also Angular apps. Angular is a JavaScript framework; therefore, it will be characterized with the discussed limitations unless mitigated with TypeScript.
Now that you know the problem at hand, buckle up while we dig Angular with the possible solutions that TypeScript provides.
So far, so good! We have been able to discuss the following concerns to help us move forward:
- Understanding loose types
- Differences between loose types and strict types
- Challenges of loosely typed programming languages, including JavaScript
- Mitigating the effects of loose types