Detection

Detection refers to the practice of determining a value's type. Usually, this will be done with the intention of using that determined type to carry out specific behavior such as falling back to a default value or throwing an error in the case of misuse.

Due to JavaScript's dynamic nature, detecting types is an important practice that can often be a great aid to other programmers. If you can usefully throw errors or warnings when someone is using an interface incorrectly, it can mean a much more fluid and speedy flow of development for them. And if you can helpfully populate undefined, null, or empty values with smart defaults, then it'll allow you to provide a more seamless and intuitive interface.

Unfortunately, due to legacies within JavaScript, and some choices made in its design, detecting types can be challenging. A number of different approaches are used, some of which are not considered best practice. We will be going over all of these practices within this section. First, however, it's worth discussing one fundamental question regarding detection: what exactly are you trying to detect?

We often think we require a specific type in order to carry out certain actions, but due to JavaScript's dynamic nature, we may not need to do so. In fact, doing so can lead us to create needlessly restrictive or rigid code.

Consider a function that accepts an array of people objects, like so:

registerPeopleForMarathon([
new Person({ id: 1, name: 'Marcus Wu' }),
new Person({ id: 2, name: 'Susan Smith' }),
new Person({ id: 3, name: 'Sofia Polat' })
]);

In our registerPeopleForMarathon, we may be tempted to implement some kind of check to ensure that the passed argument is of the expected type and structure:

function registerPeopleForMarathon(people) {
if (Array.isArray(people)) {
throw new Error('People is not an array');
}
for (let person in people) {
if (!(person instanceof Person)) {
throw new Error('Each person should be an instance of Person');
}
registerForMarathon(person.id, person.name);
}
}

Are these checks necessary? You may be inclined to say yes as they're ensuring our code is resilient (or defensive) toward potential error cases and is thus more reliable. But if we think about it, none of these checks are necessary to ensure the kind of reliability we're seeking. The intention of our checks, presumably, is to prevent downstream errors in the case that the wrong types or structures are passed to our function, but if we look closely at the preceding code, there are no risks of downstream errors of the types we're worried about.

The first check we conduct is Array.isArray(people) to determine whether the people value is indeed an array. We are doing this, ostensibly, so that we can safely loop through the array. But, as we discovered in the previous chapter, the for...of iteration style is not dependent on its of {...} value being an array. All it cares about is that the value is iterable. An example of this is as follows:

function* marathonPeopleGenerator() {
yield new Person({ id: 1, name: 'Marcus Wu' });
yield new Person({ id: 2, name: 'Susan Smith' });
yield new Person({ id: 3, name: 'Sofia Polat' });
}

for (let person of marathonPeopleGenerator()) {
console.log(person.name);
}

// Logged => "Marcus Wu"
// Logged => "Susan Smith"
// Logged => "Sofia Polat"

Here, we've used a generator as our iterable. This will work just an array would when being iterated over in for...of, so, technically, we could argue that our registerPeopleForMarathon function should accept such values:

// Should we allow this?
registerPeopleForMarathon(
marathonPeopleGenerator()
);

The checks we've made thus far would reject this value as it is not an array. Is there any sense in that? Do you remember the principle of abstraction and how we should be concerned with interface, not implementation? Seen this way, arguably, our registerPeopleForMarathon function does not need to know about the implementation detail of the passed value's type. It only cares that the value performs according to its needs. In this case, it needs to loop through the value via for...of, so any iterable is suitable. To check for an iterable, we might employ a helper such as this:

function isIterable(obj) {
return obj != null &&
typeof obj[Symbol.iterator] === 'function';
}

isIterable([1, 2, 3]); // => true
isIterable(marathonPeopleGenerator()); // => true

Also, consider that we are currently checking that all of our person values are instances of the Person constructor:

// ...
if (!(person instanceof Person)) {
throw new Error('Each person should be an instance of Person');
}

Is it necessary for us to explicitly check the instance in this way? Could we, instead, simply check for the properties that we wish to access? Perhaps all we need to assert is that the properties are non-falsy (empty strings, null, undefined, zero, and so on):

// ...
if (!person || !person.name || !person.id) {
throw new Error('Each person should have a name and id');
}

This check is arguably more specific to our true needs. Checks like these are often called duck-typing, that is, If it walks like a duck and it quacks like a duck, then it must be a duck. We don't always need to check for specific types; we can check for the properties, methods, and characteristics that we're truly dependent on. By doing so, we are creating code that is more flexible.

Our new checks, when integrated into our function, would look something like this:

function registerPeopleForMarathon(people) {
if (isIterable(people)) {
throw new Error('People is not iterable');
}
for (let person in people) {
if (!person || !person.name || !person.id) {
throw new Error('Each person should have a name and id');
}
registerForMarathon(person.id, person.name);
}
}

By using a more flexible isIterable check and employing duck-typing on our person objects, our registerPeopleForMarathon function can now be passed; for example, here, we have a generator that yields plain objects:

function* marathonPeopleGenerator() {
yield { id: 1, name: 'Marcus Wu' };
yield { id: 2, name: 'Susan Smith' };
yield { id: 3, name: 'Sofia Polat' };
}

registerPeopleForMarathon(
marathonPeopleGenerator()
);

This level of flexibility wouldn't have been possible if we had kept our strict type-checking in place. Stricter checks usually create more rigid code and needlessly limit flexibility. There is a balance to strike here, however. We cannot be endlessly flexible. It may even be the case that the rigidity and certainty provided by stricter type-checks enable us to ensure cleaner code in the long run. But the opposite may also be true. The balancing act of flexibility versus rigidity is one you should be constantly considering.

Generally, an interface's expectations should attempt to be as close as possible to the demands of the implementation. That is, we should not be performing detection or other checks unless the checks genuinely prevent errors within our implementation. Over-zealous checking may seem safer but may only mean that future requirements and use cases are more awkward to accommodate.

Now that we've covered the question of why we detect things and exposed some use cases, we can begin to cover the techniques of detection that JavaScript provides us with. We'll begin with the typeof operator.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset