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.